rust_config_tree/config_schema/
adapt.rs1use std::collections::BTreeSet;
4
5use serde_json::Value;
6
7use crate::config::ConfigResult;
8
9use super::{
10 marker::{
11 ENV_ONLY_SCHEMA_EXTENSION, TREE_INNER_FIELD_EXTENSION, TREE_SPLIT_SCHEMA_EXTENSION,
12 TREE_TRANSPARENT_ARRAY_EXTENSION,
13 },
14 paths::{direct_child_split_section_paths, inner_field_for_section},
15 reference::{
16 collect_schema_refs, collect_transitive_schema_refs, resolve_schema_reference,
17 retain_schema_map,
18 },
19};
20
21fn section_schema_for_path(root_schema: &Value, section_path: &[&str]) -> Option<Value> {
38 let property = property_schema_for_path(root_schema, section_path)?;
39 let resolved = resolve_schema_reference(root_schema, property).unwrap_or(property);
40
41 if section_has_transparent_array_marker(root_schema, section_path) {
42 return transparent_array_section_schema(root_schema, section_path, resolved);
43 }
44
45 Some(standalone_section_schema(root_schema, resolved))
46}
47
48fn property_schema_for_path<'a>(root_schema: &'a Value, path: &[&str]) -> Option<&'a Value> {
50 let mut current = root_schema;
51
52 for (index, section) in path.iter().enumerate() {
53 let property = current.get("properties")?.get(*section)?;
54 if index + 1 == path.len() {
55 return Some(property);
56 }
57
58 current = resolve_schema_reference(root_schema, property).unwrap_or(property);
59 }
60
61 None
62}
63
64fn section_has_transparent_array_marker(root_schema: &Value, section_path: &[&str]) -> bool {
66 property_schema_for_path(root_schema, section_path)
67 .and_then(|schema| schema.get(TREE_TRANSPARENT_ARRAY_EXTENSION))
68 .and_then(Value::as_bool)
69 .unwrap_or(false)
70}
71
72fn transparent_array_section_schema(
74 root_schema: &Value,
75 section_path: &[&str],
76 section_schema: &Value,
77) -> Option<Value> {
78 let inner_field = inner_field_for_section(root_schema, section_path);
79 let resolved = resolve_schema_reference(root_schema, section_schema).unwrap_or(section_schema);
80 let inner_schema = resolved
81 .get("properties")
82 .and_then(|properties| properties.get(inner_field))
83 .or_else(|| resolved.get("items"))?;
84 let inner_schema = resolve_schema_reference(root_schema, inner_schema).unwrap_or(inner_schema);
85
86 Some(standalone_section_schema(
87 root_schema,
88 &serde_json::json!({
89 "type": "array",
90 "items": inner_schema.clone(),
91 }),
92 ))
93}
94fn standalone_section_schema(root_schema: &Value, section_schema: &Value) -> Value {
112 let mut section_schema = section_schema.clone();
113 let Some(object) = section_schema.as_object_mut() else {
114 return section_schema;
115 };
116
117 if let Some(schema_uri) = root_schema.get("$schema") {
118 object
119 .entry("$schema".to_owned())
120 .or_insert_with(|| schema_uri.clone());
121 }
122
123 if let Some(definitions) = root_schema.get("definitions") {
124 object
125 .entry("definitions".to_owned())
126 .or_insert_with(|| definitions.clone());
127 }
128
129 if let Some(defs) = root_schema.get("$defs") {
130 object
131 .entry("$defs".to_owned())
132 .or_insert_with(|| defs.clone());
133 }
134
135 section_schema
136}
137pub fn schema_for_output_path(
155 full_schema: &Value,
156 section_path: &[&'static str],
157 split_paths: &[Vec<&'static str>],
158) -> ConfigResult<Value> {
159 let mut schema = if section_path.is_empty() {
160 full_schema.clone()
161 } else {
162 section_schema_for_path(full_schema, section_path).ok_or_else(|| {
163 std::io::Error::new(
164 std::io::ErrorKind::InvalidData,
165 format!(
166 "failed to extract JSON Schema for config section {}",
167 section_path.join(".")
168 ),
169 )
170 })?
171 };
172
173 remove_child_section_properties(&mut schema, section_path, split_paths);
176 remove_env_only_properties(&mut schema);
177 remove_empty_object_properties(&mut schema);
178 prune_unused_schema_maps(&mut schema);
179 remove_schema_extensions(&mut schema);
180
181 Ok(schema)
182}
183
184fn remove_child_section_properties(
202 schema: &mut Value,
203 section_path: &[&'static str],
204 split_paths: &[Vec<&'static str>],
205) {
206 let Some(properties) = schema.get_mut("properties").and_then(Value::as_object_mut) else {
207 return;
208 };
209
210 for child_section_path in direct_child_split_section_paths(section_path, split_paths) {
211 if let Some(child_name) = child_section_path.last() {
212 properties.remove(*child_name);
213 }
214 }
215}
216
217pub fn remove_env_only_properties(value: &mut Value) {
233 match value {
234 Value::Object(object) => {
235 if let Some(properties) = object.get_mut("properties").and_then(Value::as_object_mut) {
236 properties.retain(|_, schema| {
237 !schema
238 .get(ENV_ONLY_SCHEMA_EXTENSION)
239 .and_then(Value::as_bool)
240 .unwrap_or(false)
241 });
242
243 for schema in properties.values_mut() {
244 remove_env_only_properties(schema);
245 }
246 }
247
248 for (key, child) in object.iter_mut() {
249 if key != "properties" {
250 remove_env_only_properties(child);
251 }
252 }
253 }
254 Value::Array(items) => {
255 for item in items {
256 remove_env_only_properties(item);
257 }
258 }
259 Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => {}
260 }
261}
262
263pub fn remove_empty_object_properties(schema: &mut Value) {
279 loop {
280 let root_schema = schema.clone();
281 if !remove_empty_object_properties_with_root(schema, &root_schema) {
282 break;
283 }
284 }
285}
286
287fn remove_empty_object_properties_with_root(value: &mut Value, root_schema: &Value) -> bool {
304 let mut changed = false;
305
306 match value {
307 Value::Object(object) => {
308 if let Some(properties) = object.get_mut("properties").and_then(Value::as_object_mut) {
309 let before_len = properties.len();
310 properties.retain(|_, schema| !is_empty_object_schema(root_schema, schema));
311 changed |= properties.len() != before_len;
312
313 for schema in properties.values_mut() {
314 changed |= remove_empty_object_properties_with_root(schema, root_schema);
315 }
316 }
317
318 for (key, child) in object.iter_mut() {
319 if key != "properties" {
320 changed |= remove_empty_object_properties_with_root(child, root_schema);
321 }
322 }
323 }
324 Value::Array(items) => {
325 for item in items {
326 changed |= remove_empty_object_properties_with_root(item, root_schema);
327 }
328 }
329 Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => {}
330 }
331
332 changed
333}
334
335fn is_empty_object_schema(root_schema: &Value, schema: &Value) -> bool {
352 let schema = resolve_schema_reference(root_schema, schema).unwrap_or(schema);
353 let Some(object) = schema.as_object() else {
354 return false;
355 };
356
357 let is_object = object.get("type").and_then(Value::as_str) == Some("object")
358 || object.contains_key("properties");
359 let has_properties = object
360 .get("properties")
361 .and_then(Value::as_object)
362 .is_some_and(|properties| !properties.is_empty());
363 let has_dynamic_properties =
364 object.contains_key("additionalProperties") || object.contains_key("patternProperties");
365
366 is_object && !has_properties && !has_dynamic_properties
367}
368
369pub fn prune_unused_schema_maps(schema: &mut Value) {
385 let mut definitions = BTreeSet::new();
386 let mut defs = BTreeSet::new();
387
388 collect_schema_refs(schema, false, &mut definitions, &mut defs);
389
390 loop {
391 let previous_len = definitions.len() + defs.len();
392 collect_transitive_schema_refs(schema, &mut definitions, &mut defs);
393
394 if definitions.len() + defs.len() == previous_len {
395 break;
396 }
397 }
398
399 retain_schema_map(schema, "definitions", &definitions);
400 retain_schema_map(schema, "$defs", &defs);
401}
402
403pub fn remove_schema_extensions(value: &mut Value) {
419 match value {
420 Value::Object(object) => {
421 object.remove(TREE_SPLIT_SCHEMA_EXTENSION);
422 object.remove(TREE_TRANSPARENT_ARRAY_EXTENSION);
423 object.remove(TREE_INNER_FIELD_EXTENSION);
424 object.remove(ENV_ONLY_SCHEMA_EXTENSION);
425
426 for child in object.values_mut() {
427 remove_schema_extensions(child);
428 }
429 }
430 Value::Array(items) => {
431 for item in items {
432 remove_schema_extensions(item);
433 }
434 }
435 Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => {}
436 }
437}