datadog_agent_config_overlay_model/
schema_gen.rs1use std::path::Path;
10
11use indexmap::IndexMap;
12use serde_yaml::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: &serde_yaml::Value) -> IndexMap<String, FieldInfo> {
105 let properties = doc
106 .get("properties")
107 .and_then(|v| v.as_mapping())
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: &serde_yaml::Mapping, path_parts: &[&str], out: &mut Vec<(String, FieldInfo)>) {
122 for (key, value) in mapping {
123 let key_str = match key.as_str() {
124 Some(s) => s,
125 None => continue,
126 };
127
128 let mut parts = path_parts.to_vec();
129 parts.push(key_str);
130
131 let node_type = value.get("node_type").and_then(|v| v.as_str()).unwrap_or("");
134
135 match node_type {
136 "setting" => out.push(parse_setting(&parts, value)),
137 "section" => {
138 if let Some(props) = value.get("properties").and_then(|v| v.as_mapping()) {
139 collect_entries(props, &parts, out);
140 }
141 }
142 _ => {
143 if let Some(props) = value.get("properties").and_then(|v| v.as_mapping()) {
144 collect_entries(props, &parts, out);
145 }
146 }
147 }
148 }
149}
150
151fn parse_setting(path_parts: &[&str], value: &Value) -> (String, FieldInfo) {
152 let yaml_path = path_parts.join(".");
153
154 let has_no_env_tag = value
155 .get("tags")
156 .and_then(|v| v.as_sequence())
157 .map(|tags| tags.iter().any(|t| t.as_str() == Some("no-env")))
158 .unwrap_or(false);
159
160 let explicit_env_vars: Vec<String> = value
161 .get("env_vars")
162 .and_then(|v| v.as_sequence())
163 .map(|seq| seq.iter().filter_map(|v| v.as_str()).map(|s| s.to_string()).collect())
164 .unwrap_or_default();
165
166 let env = if has_no_env_tag {
168 EnvBinding::None
169 } else if explicit_env_vars.is_empty() {
170 EnvBinding::Standard
171 } else {
172 EnvBinding::Overridden(explicit_env_vars)
173 };
174
175 let value_type = parse_value_type(value);
176 let default = value.get("default").and_then(yaml_value_to_json_str);
177 let env_parser = value.get("env_parser").and_then(|v| v.as_str()).map(EnvParser::parse);
178
179 (
180 yaml_path,
181 FieldInfo {
182 value_type,
183 env,
184 env_parser,
185 default,
186 },
187 )
188}
189
190fn parse_value_type(value: &Value) -> FieldType {
191 let ty = value.get("type").and_then(|v| v.as_str());
192 let format = value.get("format").and_then(|v| v.as_str());
193
194 if format == Some("duration") {
198 return match ty {
199 Some("string") | Some("number") | Some("integer") => FieldType::Duration,
200 other => panic!(
201 "config schema field has `format: duration` with unsupported base type {:?}; \
202 expected string, number, or integer",
203 other
204 ),
205 };
206 }
207
208 match ty {
209 Some("string") => FieldType::String,
210 Some("boolean") => FieldType::Bool,
211 Some("integer") => FieldType::Integer,
212 Some("number") => FieldType::Float,
213 Some("array") => {
214 let item_type = value.get("items").and_then(|v| v.get("type")).and_then(|v| v.as_str());
215 if item_type == Some("string") {
216 FieldType::StringList
217 } else {
218 FieldType::Unknown
219 }
220 }
221 _ => FieldType::Unknown,
222 }
223}
224
225fn yaml_value_to_json_str(value: &serde_yaml::Value) -> Option<String> {
226 match value {
227 serde_yaml::Value::Null => None,
228 serde_yaml::Value::Bool(b) => Some(b.to_string()),
229 serde_yaml::Value::Number(n) => Some(n.to_string()),
230 serde_yaml::Value::String(s) => {
231 let escaped = s.replace('\\', "\\\\").replace('"', "\\\"");
232 Some(format!("\"{}\"", escaped))
233 }
234 serde_yaml::Value::Sequence(seq) => {
235 let items: Option<Vec<String>> = seq.iter().map(yaml_value_to_json_str).collect();
236 items.map(|elems| format!("[{}]", elems.join(",")))
237 }
238 serde_yaml::Value::Mapping(map) if map.is_empty() => Some("{}".to_string()),
239 _ => None,
240 }
241}
242
243pub fn field_type_as_rust(ft: &FieldType) -> &'static str {
245 match ft {
246 FieldType::String | FieldType::Unknown => "ValueType::String",
247 FieldType::Bool => "ValueType::Bool",
248 FieldType::Integer => "ValueType::Integer",
249 FieldType::Float => "ValueType::Float",
250 FieldType::StringList => "ValueType::StringList",
251 FieldType::Duration => "ValueType::Duration",
252 }
253}
254
255pub fn is_unknown(ft: &FieldType) -> bool {
257 matches!(ft, FieldType::Unknown)
258}
259
260pub fn escape_str(s: &str) -> String {
262 s.replace('\\', "\\\\").replace('"', "\\\"")
263}
264
265pub fn generate_schema_rs(schema_map: &IndexMap<String, FieldInfo>, dir: &Path) {
270 use std::fmt::Write as _;
271
272 let mut out = String::new();
273 writeln!(
274 out,
275 "// @generated by build.rs from core_schema.yaml + schema_overlay.yaml — DO NOT EDIT"
276 )
277 .unwrap();
278 writeln!(out).unwrap();
279
280 let mut keys: Vec<&str> = schema_map.keys().map(|s| s.as_str()).collect();
281 keys.sort_unstable();
282
283 for yaml_path in &keys {
284 let info = &schema_map[*yaml_path];
285 let const_name = yaml_path_to_const(yaml_path);
286 let vt = field_type_as_rust(&info.value_type);
287
288 if is_unknown(&info.value_type) {
289 writeln!(
290 out,
291 "// TODO: unknown type for '{}' — set value_type_override in the annotation",
292 yaml_path
293 )
294 .unwrap();
295 }
296
297 let env_vars_lit = match &info.env {
300 EnvBinding::Overridden(vars) => {
301 let items: Vec<String> = vars.iter().map(|e| format!("\"{}\"", escape_str(e))).collect();
302 format!("&[{}]", items.join(", "))
303 }
304 EnvBinding::None | EnvBinding::Standard => "&[]".to_string(),
305 };
306
307 let default_lit = match &info.default {
308 Some(d) => format!("Some(\"{}\")", escape_str(d)),
309 None => "None".to_string(),
310 };
311
312 writeln!(out, "pub const {}: SchemaEntry = SchemaEntry {{", const_name).unwrap();
313 writeln!(out, " schema: Schema::Datadog,").unwrap();
314 writeln!(out, " yaml_path: \"{}\",", yaml_path).unwrap();
315 writeln!(out, " env_vars: {},", env_vars_lit).unwrap();
316 writeln!(out, " value_type: {},", vt).unwrap();
317 writeln!(out, " default: {},", default_lit).unwrap();
318 writeln!(out, "}};").unwrap();
319 writeln!(out).unwrap();
320 }
321
322 let path = dir.join("schema.rs");
323 std::fs::write(&path, out).unwrap_or_else(|e| panic!("cannot write {}: {}", path.display(), e));
324}
325
326pub fn yaml_path_to_const(yaml_path: &str) -> String {
329 yaml_path
330 .chars()
331 .map(|c| if c == '.' || c == '-' { '_' } else { c })
332 .collect::<String>()
333 .to_uppercase()
334}