use std::collections::BTreeSet;
use serde_json::Value;
use crate::config::ConfigResult;
use super::{
marker::{
ENV_ONLY_SCHEMA_EXTENSION, TREE_INNER_FIELD_EXTENSION, TREE_SPLIT_SCHEMA_EXTENSION,
TREE_TRANSPARENT_ARRAY_EXTENSION,
},
paths::{direct_child_split_section_paths, inner_field_for_section},
reference::{
collect_schema_refs, collect_transitive_schema_refs, resolve_schema_reference,
retain_schema_map,
},
};
fn section_schema_for_path(root_schema: &Value, section_path: &[&str]) -> Option<Value> {
let property = property_schema_for_path(root_schema, section_path)?;
let resolved = resolve_schema_reference(root_schema, property).unwrap_or(property);
if section_has_transparent_array_marker(root_schema, section_path) {
return transparent_array_section_schema(root_schema, section_path, resolved);
}
Some(standalone_section_schema(root_schema, resolved))
}
fn property_schema_for_path<'a>(root_schema: &'a Value, path: &[&str]) -> Option<&'a Value> {
let mut current = root_schema;
for (index, section) in path.iter().enumerate() {
let property = current.get("properties")?.get(*section)?;
if index + 1 == path.len() {
return Some(property);
}
current = resolve_schema_reference(root_schema, property).unwrap_or(property);
}
None
}
fn section_has_transparent_array_marker(root_schema: &Value, section_path: &[&str]) -> bool {
property_schema_for_path(root_schema, section_path)
.and_then(|schema| schema.get(TREE_TRANSPARENT_ARRAY_EXTENSION))
.and_then(Value::as_bool)
.unwrap_or(false)
}
fn transparent_array_section_schema(
root_schema: &Value,
section_path: &[&str],
section_schema: &Value,
) -> Option<Value> {
let inner_field = inner_field_for_section(root_schema, section_path);
let resolved = resolve_schema_reference(root_schema, section_schema).unwrap_or(section_schema);
let inner_schema = resolved
.get("properties")
.and_then(|properties| properties.get(inner_field))
.or_else(|| resolved.get("items"))?;
let inner_schema = resolve_schema_reference(root_schema, inner_schema).unwrap_or(inner_schema);
Some(standalone_section_schema(
root_schema,
&serde_json::json!({
"type": "array",
"items": inner_schema.clone(),
}),
))
}
fn standalone_section_schema(root_schema: &Value, section_schema: &Value) -> Value {
let mut section_schema = section_schema.clone();
let Some(object) = section_schema.as_object_mut() else {
return section_schema;
};
if let Some(schema_uri) = root_schema.get("$schema") {
object
.entry("$schema".to_owned())
.or_insert_with(|| schema_uri.clone());
}
if let Some(definitions) = root_schema.get("definitions") {
object
.entry("definitions".to_owned())
.or_insert_with(|| definitions.clone());
}
if let Some(defs) = root_schema.get("$defs") {
object
.entry("$defs".to_owned())
.or_insert_with(|| defs.clone());
}
section_schema
}
pub fn schema_for_output_path(
full_schema: &Value,
section_path: &[&'static str],
split_paths: &[Vec<&'static str>],
) -> ConfigResult<Value> {
let mut schema = if section_path.is_empty() {
full_schema.clone()
} else {
section_schema_for_path(full_schema, section_path).ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!(
"failed to extract JSON Schema for config section {}",
section_path.join(".")
),
)
})?
};
remove_child_section_properties(&mut schema, section_path, split_paths);
remove_env_only_properties(&mut schema);
remove_empty_object_properties(&mut schema);
prune_unused_schema_maps(&mut schema);
remove_schema_extensions(&mut schema);
Ok(schema)
}
fn remove_child_section_properties(
schema: &mut Value,
section_path: &[&'static str],
split_paths: &[Vec<&'static str>],
) {
let Some(properties) = schema.get_mut("properties").and_then(Value::as_object_mut) else {
return;
};
for child_section_path in direct_child_split_section_paths(section_path, split_paths) {
if let Some(child_name) = child_section_path.last() {
properties.remove(*child_name);
}
}
}
pub fn remove_env_only_properties(value: &mut Value) {
match value {
Value::Object(object) => {
if let Some(properties) = object.get_mut("properties").and_then(Value::as_object_mut) {
properties.retain(|_, schema| {
!schema
.get(ENV_ONLY_SCHEMA_EXTENSION)
.and_then(Value::as_bool)
.unwrap_or(false)
});
for schema in properties.values_mut() {
remove_env_only_properties(schema);
}
}
for (key, child) in object.iter_mut() {
if key != "properties" {
remove_env_only_properties(child);
}
}
}
Value::Array(items) => {
for item in items {
remove_env_only_properties(item);
}
}
Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => {}
}
}
pub fn remove_empty_object_properties(schema: &mut Value) {
loop {
let root_schema = schema.clone();
if !remove_empty_object_properties_with_root(schema, &root_schema) {
break;
}
}
}
fn remove_empty_object_properties_with_root(value: &mut Value, root_schema: &Value) -> bool {
let mut changed = false;
match value {
Value::Object(object) => {
if let Some(properties) = object.get_mut("properties").and_then(Value::as_object_mut) {
let before_len = properties.len();
properties.retain(|_, schema| !is_empty_object_schema(root_schema, schema));
changed |= properties.len() != before_len;
for schema in properties.values_mut() {
changed |= remove_empty_object_properties_with_root(schema, root_schema);
}
}
for (key, child) in object.iter_mut() {
if key != "properties" {
changed |= remove_empty_object_properties_with_root(child, root_schema);
}
}
}
Value::Array(items) => {
for item in items {
changed |= remove_empty_object_properties_with_root(item, root_schema);
}
}
Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => {}
}
changed
}
fn is_empty_object_schema(root_schema: &Value, schema: &Value) -> bool {
let schema = resolve_schema_reference(root_schema, schema).unwrap_or(schema);
let Some(object) = schema.as_object() else {
return false;
};
let is_object = object.get("type").and_then(Value::as_str) == Some("object")
|| object.contains_key("properties");
let has_properties = object
.get("properties")
.and_then(Value::as_object)
.is_some_and(|properties| !properties.is_empty());
let has_dynamic_properties =
object.contains_key("additionalProperties") || object.contains_key("patternProperties");
is_object && !has_properties && !has_dynamic_properties
}
pub fn prune_unused_schema_maps(schema: &mut Value) {
let mut definitions = BTreeSet::new();
let mut defs = BTreeSet::new();
collect_schema_refs(schema, false, &mut definitions, &mut defs);
loop {
let previous_len = definitions.len() + defs.len();
collect_transitive_schema_refs(schema, &mut definitions, &mut defs);
if definitions.len() + defs.len() == previous_len {
break;
}
}
retain_schema_map(schema, "definitions", &definitions);
retain_schema_map(schema, "$defs", &defs);
}
pub fn remove_schema_extensions(value: &mut Value) {
match value {
Value::Object(object) => {
object.remove(TREE_SPLIT_SCHEMA_EXTENSION);
object.remove(TREE_TRANSPARENT_ARRAY_EXTENSION);
object.remove(TREE_INNER_FIELD_EXTENSION);
object.remove(ENV_ONLY_SCHEMA_EXTENSION);
for child in object.values_mut() {
remove_schema_extensions(child);
}
}
Value::Array(items) => {
for item in items {
remove_schema_extensions(item);
}
}
Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => {}
}
}