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