#[must_use]
pub fn apply_defaults(schema: &serde_json::Value, value: &serde_json::Value) -> serde_json::Value {
apply_defaults_inner(schema, value, 0)
}
fn apply_defaults_inner(
schema: &serde_json::Value,
value: &serde_json::Value,
depth: usize,
) -> serde_json::Value {
if depth > 64 {
return value.clone();
}
match value {
serde_json::Value::Object(obj) => {
let props = match schema.get("properties").and_then(|p| p.as_object()) {
Some(p) => p,
None => return value.clone(),
};
let has_missing_defaults = props
.iter()
.any(|(key, prop_schema)| !obj.contains_key(key) && prop_schema.get("default").is_some());
let has_children_to_recurse = props.keys().any(|key| obj.contains_key(key));
if !has_missing_defaults && !has_children_to_recurse {
return value.clone();
}
let mut result = obj.clone();
for (key, prop_schema) in props {
match result.get(key) {
Some(child) => {
let defaulted = apply_defaults_inner(prop_schema, child, depth + 1);
result.insert(key.clone(), defaulted);
}
None => {
if let Some(default_val) = prop_schema.get("default") {
result.insert(key.clone(), default_val.clone());
}
}
}
}
serde_json::Value::Object(result)
}
serde_json::Value::Array(arr) => {
if let Some(items_schema) = schema.get("items") {
let items: Vec<_> = arr
.iter()
.map(|item| apply_defaults_inner(items_schema, item, depth + 1))
.collect();
serde_json::Value::Array(items)
} else {
value.clone()
}
}
_ => value.clone(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn apply_defaults_fills_missing_field() {
let schema = json!({
"type": "object",
"properties": {
"timeout": {"type": "string", "default": "30s"},
"name": {"type": "string"}
}
});
let value = json!({"name": "test"});
let result = apply_defaults(&schema, &value);
assert_eq!(result, json!({"name": "test", "timeout": "30s"}));
}
#[test]
fn apply_defaults_does_not_overwrite_existing() {
let schema = json!({
"type": "object",
"properties": {
"timeout": {"type": "string", "default": "30s"}
}
});
let value = json!({"timeout": "60s"});
let result = apply_defaults(&schema, &value);
assert_eq!(result, json!({"timeout": "60s"}));
}
#[test]
fn apply_defaults_nested_object() {
let schema = json!({
"type": "object",
"properties": {
"spec": {
"type": "object",
"properties": {
"replicas": {"type": "integer", "default": 1}
}
}
}
});
let value = json!({"spec": {}});
let result = apply_defaults(&schema, &value);
assert_eq!(result, json!({"spec": {"replicas": 1}}));
}
#[test]
fn apply_defaults_array_items() {
let schema = json!({
"type": "array",
"items": {
"type": "object",
"properties": {
"port": {"type": "integer", "default": 80}
}
}
});
let value = json!([{"port": 443}, {}]);
let result = apply_defaults(&schema, &value);
assert_eq!(result, json!([{"port": 443}, {"port": 80}]));
}
#[test]
fn apply_defaults_no_schema_properties() {
let schema = json!({"type": "object"});
let value = json!({"x": 1});
let result = apply_defaults(&schema, &value);
assert_eq!(result, json!({"x": 1}));
}
#[test]
fn apply_defaults_non_object_passthrough() {
let schema = json!({"type": "string", "default": "hello"});
let value = json!("world");
let result = apply_defaults(&schema, &value);
assert_eq!(result, json!("world"));
}
}