use serde_json::{Map, Value};
use std::collections::HashSet;
use thiserror::Error;
pub const MAX_REF_DEPTH: usize = 32;
#[derive(Debug, Error)]
pub enum RefResolverError {
#[error("unresolvable $ref '{reference}' in module '{module_id}' (exit 45)")]
Unresolvable {
reference: String,
module_id: String,
},
#[error("circular $ref detected in module '{module_id}' (exit 48)")]
Circular { module_id: String },
#[error("$ref resolution exceeded max depth {max_depth} in module '{module_id}'")]
MaxDepthExceeded { max_depth: usize, module_id: String },
}
pub fn resolve_refs(
schema: &Value,
max_depth: usize,
module_id: &str,
) -> Result<Value, RefResolverError> {
let copy = schema.clone();
let defs: Map<String, Value> = copy
.get("$defs")
.or_else(|| copy.get("definitions"))
.and_then(|v| v.as_object())
.cloned()
.unwrap_or_default();
let mut visiting: HashSet<String> = HashSet::new();
let resolved = resolve_node(copy, &defs, 0, max_depth, &mut visiting, module_id)?;
let mut result = resolved;
if let Some(obj) = result.as_object_mut() {
obj.remove("$defs");
obj.remove("definitions");
}
Ok(result)
}
fn merge_allof(parent_required: &[Value], branches: Vec<Value>) -> Value {
let mut merged_props = Map::new();
let mut merged_required: Vec<Value> = Vec::new();
for item in parent_required {
if !merged_required.contains(item) {
merged_required.push(item.clone());
}
}
for branch in branches {
if let Some(props) = branch.get("properties").and_then(|v| v.as_object()) {
for (k, v) in props {
merged_props.insert(k.clone(), v.clone());
}
}
if let Some(req) = branch.get("required").and_then(|v| v.as_array()) {
for item in req {
if !merged_required.contains(item) {
merged_required.push(item.clone());
}
}
}
}
let mut result = Map::new();
result.insert("properties".to_string(), Value::Object(merged_props));
result.insert("required".to_string(), Value::Array(merged_required));
Value::Object(result)
}
fn intersect_required_sets(sets: Vec<HashSet<String>>) -> Vec<Value> {
if sets.is_empty() {
return Vec::new();
}
let mut iter = sets.into_iter();
let first = iter.next().unwrap();
iter.fold(first, |acc, set| acc.intersection(&set).cloned().collect())
.into_iter()
.map(Value::String)
.collect()
}
fn merge_anyof(branches: Vec<Value>) -> Value {
let mut merged_props = Map::new();
let mut all_required_sets: Vec<HashSet<String>> = Vec::new();
for branch in branches {
if let Some(props) = branch.get("properties").and_then(|v| v.as_object()) {
for (k, v) in props {
merged_props.insert(k.clone(), v.clone());
}
}
let set: HashSet<String> = branch
.get("required")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(str::to_string))
.collect()
})
.unwrap_or_default();
all_required_sets.push(set);
}
let intersection = intersect_required_sets(all_required_sets);
let mut result = Map::new();
result.insert("properties".to_string(), Value::Object(merged_props));
result.insert("required".to_string(), Value::Array(intersection));
Value::Object(result)
}
fn resolve_node(
node: Value,
defs: &Map<String, Value>,
depth: usize,
max_depth: usize,
visiting: &mut HashSet<String>,
module_id: &str,
) -> Result<Value, RefResolverError> {
let obj = match node {
Value::Object(map) => map,
other => return Ok(other),
};
if let Some(ref_val) = obj.get("$ref") {
let ref_path = ref_val.as_str().unwrap_or("").to_string();
if depth >= max_depth {
return Err(RefResolverError::MaxDepthExceeded {
max_depth,
module_id: module_id.to_string(),
});
}
if visiting.contains(&ref_path) {
return Err(RefResolverError::Circular {
module_id: module_id.to_string(),
});
}
let key = ref_path.split('/').next_back().unwrap_or("").to_string();
let def = defs
.get(&key)
.cloned()
.ok_or_else(|| RefResolverError::Unresolvable {
reference: ref_path.clone(),
module_id: module_id.to_string(),
})?;
visiting.insert(ref_path.clone());
let result = resolve_node(def, defs, depth + 1, max_depth, visiting, module_id)?;
visiting.remove(&ref_path);
return Ok(result);
}
if obj.contains_key("allOf") {
let sub_schemas = obj
.get("allOf")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
let mut resolved_branches = Vec::with_capacity(sub_schemas.len());
for sub in sub_schemas {
let resolved_sub = resolve_node(sub, defs, depth + 1, max_depth, visiting, module_id)?;
resolved_branches.push(resolved_sub);
}
let parent_required = obj
.get("required")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
let merged = merge_allof(&parent_required, resolved_branches);
let merged_map = match merged {
Value::Object(m) => m,
_ => Map::new(),
};
let mut result_map = merged_map;
if let Some(parent_props) = obj.get("properties").and_then(|v| v.as_object()) {
if let Some(Value::Object(merged_props)) = result_map.get_mut("properties") {
for (k, v) in parent_props {
merged_props.entry(k.clone()).or_insert_with(|| v.clone());
}
}
}
for (k, v) in &obj {
if k != "allOf" && !result_map.contains_key(k) {
result_map.insert(k.clone(), v.clone());
}
}
return Ok(Value::Object(result_map));
}
for keyword in &["anyOf", "oneOf"] {
if obj.contains_key(*keyword) {
let sub_schemas = obj
.get(*keyword)
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
let mut resolved_branches = Vec::with_capacity(sub_schemas.len());
for sub in sub_schemas {
let resolved_sub =
resolve_node(sub, defs, depth + 1, max_depth, visiting, module_id)?;
resolved_branches.push(resolved_sub);
}
let merged = merge_anyof(resolved_branches);
let merged_map = match merged {
Value::Object(m) => m,
_ => Map::new(),
};
let mut result_map = merged_map;
if let Some(parent_props) = obj.get("properties").and_then(|v| v.as_object()) {
if let Some(Value::Object(merged_props)) = result_map.get_mut("properties") {
for (k, v) in parent_props {
merged_props.entry(k.clone()).or_insert_with(|| v.clone());
}
}
}
if let Some(parent_req) = obj.get("required").and_then(|v| v.as_array()) {
if let Some(Value::Array(merged_req)) = result_map.get_mut("required") {
let mut combined: Vec<Value> = Vec::new();
let mut seen: HashSet<String> = HashSet::new();
for item in parent_req.iter().chain(merged_req.iter()) {
if let Some(s) = item.as_str() {
if seen.insert(s.to_string()) {
combined.push(item.clone());
}
}
}
*merged_req = combined;
}
}
for (k, v) in &obj {
if k != *keyword && !result_map.contains_key(k) {
result_map.insert(k.clone(), v.clone());
}
}
return Ok(Value::Object(result_map));
}
}
let mut resolved_map = Map::with_capacity(obj.len());
for (k, v) in obj {
let resolved_v = resolve_node(v, defs, depth, max_depth, visiting, module_id)?;
resolved_map.insert(k, resolved_v);
}
Ok(Value::Object(resolved_map))
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_resolve_refs_no_refs_unchanged() {
let schema = json!({
"type": "object",
"properties": {
"name": {"type": "string"}
}
});
let result = resolve_refs(&schema, 32, "test.module");
assert!(result.is_ok());
let resolved = result.unwrap();
assert_eq!(resolved["properties"]["name"]["type"], "string");
}
#[test]
fn test_resolve_refs_simple_ref() {
let schema = json!({
"$defs": {
"MyString": {"type": "string", "description": "A name"}
},
"type": "object",
"properties": {
"name": {"$ref": "#/$defs/MyString"}
}
});
let result = resolve_refs(&schema, 32, "test.module");
assert!(result.is_ok());
let resolved = result.unwrap();
assert_eq!(resolved["properties"]["name"]["type"], "string");
assert_eq!(resolved["properties"]["name"]["description"], "A name");
assert!(resolved.get("$defs").is_none());
}
#[test]
fn test_resolve_refs_definitions_key_also_supported() {
let schema = json!({
"definitions": {
"Addr": {"type": "string"}
},
"properties": {
"city": {"$ref": "#/definitions/Addr"}
}
});
let result = resolve_refs(&schema, 32, "test.module");
assert!(result.is_ok());
let resolved = result.unwrap();
assert_eq!(resolved["properties"]["city"]["type"], "string");
assert!(resolved.get("definitions").is_none());
}
#[test]
fn test_resolve_refs_unresolvable_returns_error() {
let schema = json!({
"type": "object",
"properties": {
"x": {"$ref": "#/$defs/DoesNotExist"}
}
});
let result = resolve_refs(&schema, 32, "test.module");
assert!(
matches!(result, Err(RefResolverError::Unresolvable { .. })),
"expected Unresolvable, got: {result:?}"
);
}
#[test]
fn test_resolve_refs_circular_returns_error() {
let schema = json!({
"$defs": {
"A": {"$ref": "#/$defs/B"},
"B": {"$ref": "#/$defs/A"}
},
"properties": {
"x": {"$ref": "#/$defs/A"}
}
});
let result = resolve_refs(&schema, 32, "test.module");
assert!(
matches!(
result,
Err(RefResolverError::Circular { .. })
| Err(RefResolverError::MaxDepthExceeded { .. })
),
"expected Circular or MaxDepthExceeded, got: {result:?}"
);
}
#[test]
fn test_resolve_refs_max_depth_exceeded() {
let schema = json!({
"$defs": {
"Inner": {"type": "string"}
},
"properties": {
"x": {"$ref": "#/$defs/Inner"}
}
});
let result = resolve_refs(&schema, 0, "test.module");
assert!(
matches!(result, Err(RefResolverError::MaxDepthExceeded { .. })),
"expected MaxDepthExceeded, got: {result:?}"
);
}
#[test]
fn test_resolve_refs_nested_defs() {
let schema = json!({
"$defs": {
"City": {"type": "string"}
},
"properties": {
"address": {
"type": "object",
"properties": {
"city": {"$ref": "#/$defs/City"}
}
}
}
});
let result = resolve_refs(&schema, 32, "test.module");
assert!(result.is_ok());
let resolved = result.unwrap();
assert_eq!(
resolved["properties"]["address"]["properties"]["city"]["type"],
"string"
);
}
#[test]
fn test_resolve_refs_does_not_mutate_input() {
let schema = json!({
"$defs": {"T": {"type": "integer"}},
"properties": {"x": {"$ref": "#/$defs/T"}}
});
let _ = resolve_refs(&schema, 32, "test.module");
assert_eq!(schema["properties"]["x"]["$ref"], "#/$defs/T");
}
#[test]
fn test_resolve_refs_sibling_refs_same_def() {
let schema = json!({
"$defs": {
"Str": {"type": "string"}
},
"properties": {
"a": {"$ref": "#/$defs/Str"},
"b": {"$ref": "#/$defs/Str"}
}
});
let result = resolve_refs(&schema, 32, "test.module");
assert!(result.is_ok(), "sibling refs failed: {result:?}");
let resolved = result.unwrap();
assert_eq!(resolved["properties"]["a"]["type"], "string");
assert_eq!(resolved["properties"]["b"]["type"], "string");
}
#[test]
fn test_allof_merges_properties() {
let schema = json!({
"allOf": [
{
"properties": {"a": {"type": "string"}},
"required": ["a"]
},
{
"properties": {"b": {"type": "integer"}},
"required": ["b"]
}
]
});
let result = resolve_refs(&schema, 32, "mod").unwrap();
assert_eq!(result["properties"]["a"]["type"], "string");
assert_eq!(result["properties"]["b"]["type"], "integer");
let required: Vec<&str> = result["required"]
.as_array()
.unwrap()
.iter()
.filter_map(|v| v.as_str())
.collect();
assert!(required.contains(&"a"));
assert!(required.contains(&"b"));
}
#[test]
fn test_allof_later_schema_wins_on_conflict() {
let schema = json!({
"allOf": [
{"properties": {"x": {"type": "string"}}},
{"properties": {"x": {"type": "integer"}}}
]
});
let result = resolve_refs(&schema, 32, "mod").unwrap();
assert_eq!(result["properties"]["x"]["type"], "integer");
}
#[test]
fn test_allof_copies_non_composition_keys() {
let schema = json!({
"description": "My type",
"allOf": [
{"properties": {"a": {"type": "string"}}}
]
});
let result = resolve_refs(&schema, 32, "mod").unwrap();
assert_eq!(result["description"], "My type");
}
#[test]
fn test_anyof_unions_properties() {
let schema = json!({
"anyOf": [
{"properties": {"a": {"type": "string"}}, "required": ["a"]},
{"properties": {"b": {"type": "integer"}}, "required": ["b"]}
]
});
let result = resolve_refs(&schema, 32, "mod").unwrap();
assert!(result["properties"].get("a").is_some());
assert!(result["properties"].get("b").is_some());
}
#[test]
fn test_anyof_required_is_intersection() {
let schema = json!({
"anyOf": [
{"properties": {"a": {"type": "string"}, "b": {"type": "string"}}, "required": ["a", "b"]},
{"properties": {"a": {"type": "string"}, "c": {"type": "string"}}, "required": ["a", "c"]}
]
});
let result = resolve_refs(&schema, 32, "mod").unwrap();
let required: Vec<&str> = result["required"]
.as_array()
.unwrap()
.iter()
.filter_map(|v| v.as_str())
.collect();
assert!(
required.contains(&"a"),
"a must be required (in both branches)"
);
assert!(
!required.contains(&"b"),
"b must not be required (only in first branch)"
);
assert!(
!required.contains(&"c"),
"c must not be required (only in second branch)"
);
}
#[test]
fn test_anyof_empty_required_when_no_overlap() {
let schema = json!({
"anyOf": [
{"properties": {"a": {"type": "string"}}, "required": ["a"]},
{"properties": {"b": {"type": "integer"}}, "required": ["b"]}
]
});
let result = resolve_refs(&schema, 32, "mod").unwrap();
let required = result["required"].as_array().unwrap();
assert!(
required.is_empty(),
"no fields are required in both branches"
);
}
#[test]
fn test_oneof_behaves_like_anyof() {
let schema = json!({
"oneOf": [
{"properties": {"x": {"type": "string"}}, "required": ["x"]},
{"properties": {"y": {"type": "integer"}}, "required": ["y"]}
]
});
let result = resolve_refs(&schema, 32, "mod").unwrap();
assert!(result["properties"].get("x").is_some());
assert!(result["properties"].get("y").is_some());
assert!(result["required"].as_array().unwrap().is_empty());
}
#[test]
fn test_allof_with_nested_ref() {
let schema = json!({
"$defs": {
"Base": {"properties": {"id": {"type": "integer"}}, "required": ["id"]}
},
"allOf": [
{"$ref": "#/$defs/Base"},
{"properties": {"name": {"type": "string"}}}
]
});
let result = resolve_refs(&schema, 32, "mod").unwrap();
assert_eq!(result["properties"]["id"]["type"], "integer");
assert_eq!(result["properties"]["name"]["type"], "string");
let required: Vec<&str> = result["required"]
.as_array()
.unwrap()
.iter()
.filter_map(|v| v.as_str())
.collect();
assert!(required.contains(&"id"));
}
#[test]
fn test_anyof_preserves_parent_sibling_required() {
let schema = json!({
"type": "object",
"required": ["x"],
"anyOf": [
{"properties": {"a": {"type": "string"}}, "required": ["a"]},
{"properties": {"a": {"type": "integer"}}, "required": ["a"]},
]
});
let result = resolve_refs(&schema, 32, "mod").unwrap();
let required: Vec<&str> = result["required"]
.as_array()
.unwrap()
.iter()
.filter_map(|v| v.as_str())
.collect();
assert_eq!(required, vec!["x", "a"]);
}
#[test]
fn test_oneof_preserves_parent_sibling_required() {
let schema = json!({
"type": "object",
"required": ["host", "port"],
"oneOf": [
{"properties": {"mode": {"const": "http"}}, "required": ["scheme"]},
{"properties": {"mode": {"const": "tcp"}}, "required": ["scheme"]},
]
});
let result = resolve_refs(&schema, 32, "mod").unwrap();
let required: Vec<&str> = result["required"]
.as_array()
.unwrap()
.iter()
.filter_map(|v| v.as_str())
.collect();
assert_eq!(required, vec!["host", "port", "scheme"]);
}
#[test]
fn test_anyof_dedupes_overlap_between_sibling_and_branch_intersection() {
let schema = json!({
"type": "object",
"required": ["a"],
"anyOf": [
{"required": ["a", "b"]},
{"required": ["a", "c"]},
]
});
let result = resolve_refs(&schema, 32, "mod").unwrap();
let required: Vec<&str> = result["required"]
.as_array()
.unwrap()
.iter()
.filter_map(|v| v.as_str())
.collect();
assert_eq!(required, vec!["a"]);
}
#[test]
fn test_allof_required_parent_first_ordering() {
let schema = json!({
"type": "object",
"required": ["x"],
"allOf": [
{"properties": {"a": {"type": "string"}}, "required": ["a"]},
{"properties": {"b": {"type": "integer"}}, "required": ["b"]},
]
});
let result = resolve_refs(&schema, 32, "mod").unwrap();
let required: Vec<&str> = result["required"]
.as_array()
.unwrap()
.iter()
.filter_map(|v| v.as_str())
.collect();
assert_eq!(required, vec!["x", "a", "b"]);
}
}