use std::borrow::Cow;
use serde_json::{Map, Value};
pub fn strip_schema_keyword(schema: &mut Value) {
if let Some(obj) = schema.as_object_mut() {
obj.remove("$schema");
}
recurse_into_subschemas(schema, strip_schema_keyword);
}
pub fn strip_conditional_keywords(schema: &mut Value) {
if let Some(obj) = schema.as_object_mut() {
obj.remove("if");
obj.remove("then");
obj.remove("else");
}
recurse_into_subschemas(schema, strip_conditional_keywords);
}
pub fn add_implicit_object_type(schema: &mut Value) {
if let Some(obj) = schema.as_object_mut() {
if obj.contains_key("properties") && !obj.contains_key("type") {
obj.insert("type".to_string(), Value::String("object".to_string()));
}
}
recurse_into_subschemas(schema, add_implicit_object_type);
}
pub fn convert_const_to_enum(schema: &mut Value) {
if let Some(obj) = schema.as_object_mut() {
if let Some(const_val) = obj.remove("const") {
obj.insert("enum".to_string(), Value::Array(vec![const_val]));
}
}
recurse_into_subschemas(schema, convert_const_to_enum);
}
pub fn strip_unsupported_formats(schema: &mut Value, allowed: &[&str]) {
if let Some(obj) = schema.as_object_mut() {
let should_remove =
obj.get("format").and_then(|f| f.as_str()).is_some_and(|f| !allowed.contains(&f));
if should_remove {
obj.remove("format");
}
}
recurse_into_subschemas_with_context(schema, allowed, strip_unsupported_formats);
}
pub fn truncate_tool_name(name: &str, max_bytes: usize) -> Cow<'_, str> {
if name.len() <= max_bytes {
Cow::Borrowed(name)
} else {
let mut end = max_bytes;
while end > 0 && !name.is_char_boundary(end) {
end -= 1;
}
Cow::Owned(name[..end].to_string())
}
}
pub fn strip_null_from_enum(schema: &mut Value) {
if let Some(obj) = schema.as_object_mut() {
if let Some(enum_val) = obj.get_mut("enum") {
if let Some(arr) = enum_val.as_array_mut() {
arr.retain(|v| !v.is_null());
if arr.is_empty() {
obj.remove("enum");
}
}
}
}
recurse_into_subschemas(schema, strip_null_from_enum);
}
pub fn resolve_refs(schema: &mut Value, definitions: &Map<String, Value>, depth: usize) {
if depth > 10 {
if schema.as_object().is_some_and(|obj| obj.contains_key("$ref")) {
*schema = serde_json::json!({"type": "object"});
}
return;
}
let Some(obj) = schema.as_object() else {
return;
};
if let Some(ref_val) = obj.get("$ref").and_then(|v| v.as_str()) {
let name =
ref_val.strip_prefix("#/definitions/").or_else(|| ref_val.strip_prefix("#/$defs/"));
if let Some(def_name) = name {
if let Some(def_schema) = definitions.get(def_name) {
*schema = def_schema.clone();
} else {
*schema = serde_json::json!({"type": "object"});
}
} else {
*schema = serde_json::json!({"type": "object"});
}
resolve_refs(schema, definitions, depth + 1);
} else {
resolve_refs_recurse(schema, definitions, depth);
}
}
fn resolve_refs_recurse(schema: &mut Value, definitions: &Map<String, Value>, depth: usize) {
let Some(obj) = schema.as_object_mut() else {
return;
};
if let Some(props) = obj.get_mut("properties") {
if let Some(props_obj) = props.as_object_mut() {
for value in props_obj.values_mut() {
resolve_refs(value, definitions, depth);
}
}
}
if let Some(items) = obj.get_mut("items") {
if items.is_object() {
resolve_refs(items, definitions, depth);
} else if let Some(arr) = items.as_array_mut() {
for item in arr.iter_mut() {
resolve_refs(item, definitions, depth);
}
}
}
if let Some(additional) = obj.get_mut("additionalProperties") {
if additional.is_object() {
resolve_refs(additional, definitions, depth);
}
}
for keyword in &["allOf", "anyOf", "oneOf"] {
if let Some(arr_val) = obj.get_mut(*keyword) {
if let Some(arr) = arr_val.as_array_mut() {
for sub in arr.iter_mut() {
resolve_refs(sub, definitions, depth);
}
}
}
}
if let Some(not_schema) = obj.get_mut("not") {
if not_schema.is_object() {
resolve_refs(not_schema, definitions, depth);
}
}
if let Some(pattern_props) = obj.get_mut("patternProperties") {
if let Some(pp_obj) = pattern_props.as_object_mut() {
for value in pp_obj.values_mut() {
resolve_refs(value, definitions, depth);
}
}
}
if let Some(prefix_items) = obj.get_mut("prefixItems") {
if let Some(arr) = prefix_items.as_array_mut() {
for item in arr.iter_mut() {
resolve_refs(item, definitions, depth);
}
}
}
for keyword in &["if", "then", "else"] {
if let Some(sub) = obj.get_mut(*keyword) {
if sub.is_object() {
resolve_refs(sub, definitions, depth);
}
}
}
}
fn recurse_into_subschemas(schema: &mut Value, transform: fn(&mut Value)) {
let Some(obj) = schema.as_object_mut() else {
return;
};
if let Some(props) = obj.get_mut("properties") {
if let Some(props_obj) = props.as_object_mut() {
for value in props_obj.values_mut() {
transform(value);
}
}
}
if let Some(items) = obj.get_mut("items") {
if items.is_object() {
transform(items);
} else if let Some(arr) = items.as_array_mut() {
for item in arr.iter_mut() {
transform(item);
}
}
}
if let Some(additional) = obj.get_mut("additionalProperties") {
if additional.is_object() {
transform(additional);
}
}
for keyword in &["allOf", "anyOf", "oneOf"] {
if let Some(arr_val) = obj.get_mut(*keyword) {
if let Some(arr) = arr_val.as_array_mut() {
for sub in arr.iter_mut() {
transform(sub);
}
}
}
}
if let Some(not_schema) = obj.get_mut("not") {
if not_schema.is_object() {
transform(not_schema);
}
}
if let Some(pattern_props) = obj.get_mut("patternProperties") {
if let Some(pp_obj) = pattern_props.as_object_mut() {
for value in pp_obj.values_mut() {
transform(value);
}
}
}
if let Some(prefix_items) = obj.get_mut("prefixItems") {
if let Some(arr) = prefix_items.as_array_mut() {
for item in arr.iter_mut() {
transform(item);
}
}
}
for keyword in &["if", "then", "else"] {
if let Some(sub) = obj.get_mut(*keyword) {
if sub.is_object() {
transform(sub);
}
}
}
}
fn recurse_into_subschemas_with_context<C: ?Sized>(
schema: &mut Value,
ctx: &C,
transform: fn(&mut Value, &C),
) {
let Some(obj) = schema.as_object_mut() else {
return;
};
if let Some(props) = obj.get_mut("properties") {
if let Some(props_obj) = props.as_object_mut() {
for value in props_obj.values_mut() {
transform(value, ctx);
}
}
}
if let Some(items) = obj.get_mut("items") {
if items.is_object() {
transform(items, ctx);
} else if let Some(arr) = items.as_array_mut() {
for item in arr.iter_mut() {
transform(item, ctx);
}
}
}
if let Some(additional) = obj.get_mut("additionalProperties") {
if additional.is_object() {
transform(additional, ctx);
}
}
for keyword in &["allOf", "anyOf", "oneOf"] {
if let Some(arr_val) = obj.get_mut(*keyword) {
if let Some(arr) = arr_val.as_array_mut() {
for sub in arr.iter_mut() {
transform(sub, ctx);
}
}
}
}
if let Some(not_schema) = obj.get_mut("not") {
if not_schema.is_object() {
transform(not_schema, ctx);
}
}
if let Some(pattern_props) = obj.get_mut("patternProperties") {
if let Some(pp_obj) = pattern_props.as_object_mut() {
for value in pp_obj.values_mut() {
transform(value, ctx);
}
}
}
if let Some(prefix_items) = obj.get_mut("prefixItems") {
if let Some(arr) = prefix_items.as_array_mut() {
for item in arr.iter_mut() {
transform(item, ctx);
}
}
}
for keyword in &["if", "then", "else"] {
if let Some(sub) = obj.get_mut(*keyword) {
if sub.is_object() {
transform(sub, ctx);
}
}
}
}
pub fn collapse_combiners(schema: &mut Value) {
let Some(obj) = schema.as_object_mut() else {
return;
};
for keyword in &["anyOf", "oneOf"] {
if let Some(arr_val) = obj.remove(*keyword) {
if let Some(arr) = arr_val.as_array() {
let chosen = arr.iter().find(|sub| !is_null_schema(sub)).or_else(|| arr.first());
if let Some(chosen_schema) = chosen {
if let Some(chosen_obj) = chosen_schema.as_object() {
for (key, value) in chosen_obj {
obj.insert(key.clone(), value.clone());
}
}
}
}
break;
}
}
recurse_into_subschemas(schema, collapse_combiners);
}
pub fn merge_all_of(schema: &mut Value) {
let Some(obj) = schema.as_object_mut() else {
return;
};
if let Some(arr_val) = obj.remove("allOf") {
if let Some(arr) = arr_val.as_array() {
let mut merged_properties = Map::new();
let mut merged_required: Vec<Value> = Vec::new();
let mut merged_type: Option<Value> = None;
let mut other_fields = Map::new();
for sub in arr {
let Some(sub_obj) = sub.as_object() else {
continue;
};
for (key, value) in sub_obj {
match key.as_str() {
"properties" => {
if let Some(props) = value.as_object() {
for (pk, pv) in props {
merged_properties.insert(pk.clone(), pv.clone());
}
}
}
"required" => {
if let Some(req_arr) = value.as_array() {
for item in req_arr {
if !merged_required.contains(item) {
merged_required.push(item.clone());
}
}
}
}
"type" => {
if let Some(existing) = &merged_type {
if existing != value {
merged_type = Some(Value::String("object".to_string()));
}
} else {
merged_type = Some(value.clone());
}
}
_ => {
other_fields.insert(key.clone(), value.clone());
}
}
}
}
for (key, value) in other_fields {
obj.entry(key).or_insert(value);
}
if let Some(type_val) = merged_type {
obj.insert("type".to_string(), type_val);
}
if !merged_properties.is_empty() {
let existing_props =
obj.entry("properties").or_insert_with(|| Value::Object(Map::new()));
if let Some(existing_obj) = existing_props.as_object_mut() {
for (key, value) in merged_properties {
existing_obj.insert(key, value);
}
}
}
if !merged_required.is_empty() {
let existing_required =
obj.entry("required").or_insert_with(|| Value::Array(Vec::new()));
if let Some(existing_arr) = existing_required.as_array_mut() {
for item in merged_required {
if !existing_arr.contains(&item) {
existing_arr.push(item);
}
}
}
}
}
}
recurse_into_subschemas(schema, merge_all_of);
}
pub fn collapse_type_arrays(schema: &mut Value) {
if let Some(obj) = schema.as_object_mut() {
if let Some(type_val) = obj.get("type").cloned() {
if let Some(arr) = type_val.as_array() {
let chosen =
arr.iter().find(|t| t.as_str() != Some("null")).or_else(|| arr.first());
if let Some(chosen_type) = chosen {
obj.insert("type".to_string(), chosen_type.clone());
}
}
}
}
recurse_into_subschemas(schema, collapse_type_arrays);
}
pub fn enforce_nesting_depth(schema: &mut Value, max_depth: usize, current: usize) {
let Some(obj) = schema.as_object_mut() else {
return;
};
let is_object_schema = obj.get("type").and_then(|t| t.as_str()).is_some_and(|t| t == "object")
|| obj.contains_key("properties");
if is_object_schema && current >= max_depth {
tracing::warn!(
depth = current,
max_depth,
"schema nesting depth exceeded, truncating to {{\"type\": \"object\"}}"
);
*schema = serde_json::json!({"type": "object"});
return;
}
let next_depth = if is_object_schema { current + 1 } else { current };
if let Some(props) = obj.get_mut("properties") {
if let Some(props_obj) = props.as_object_mut() {
for value in props_obj.values_mut() {
enforce_nesting_depth(value, max_depth, next_depth);
}
}
}
if let Some(items) = obj.get_mut("items") {
if items.is_object() {
enforce_nesting_depth(items, max_depth, next_depth);
} else if let Some(arr) = items.as_array_mut() {
for item in arr.iter_mut() {
enforce_nesting_depth(item, max_depth, next_depth);
}
}
}
if let Some(additional) = obj.get_mut("additionalProperties") {
if additional.is_object() {
enforce_nesting_depth(additional, max_depth, next_depth);
}
}
for keyword in &["allOf", "anyOf", "oneOf"] {
if let Some(arr_val) = obj.get_mut(*keyword) {
if let Some(arr) = arr_val.as_array_mut() {
for sub in arr.iter_mut() {
enforce_nesting_depth(sub, max_depth, next_depth);
}
}
}
}
if let Some(not_schema) = obj.get_mut("not") {
if not_schema.is_object() {
enforce_nesting_depth(not_schema, max_depth, next_depth);
}
}
if let Some(pattern_props) = obj.get_mut("patternProperties") {
if let Some(pp_obj) = pattern_props.as_object_mut() {
for value in pp_obj.values_mut() {
enforce_nesting_depth(value, max_depth, next_depth);
}
}
}
}
fn is_null_schema(schema: &Value) -> bool {
schema
.as_object()
.and_then(|obj| obj.get("type"))
.and_then(|t| t.as_str())
.is_some_and(|t| t == "null")
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_strip_schema_keyword_top_level() {
let mut schema = json!({
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object"
});
strip_schema_keyword(&mut schema);
assert!(schema.get("$schema").is_none());
assert_eq!(schema["type"], "object");
}
#[test]
fn test_strip_schema_keyword_nested() {
let mut schema = json!({
"type": "object",
"properties": {
"child": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "string"
}
}
});
strip_schema_keyword(&mut schema);
assert!(schema["properties"]["child"].get("$schema").is_none());
}
#[test]
fn test_strip_schema_keyword_no_op_when_absent() {
let mut schema = json!({"type": "string"});
let expected = schema.clone();
strip_schema_keyword(&mut schema);
assert_eq!(schema, expected);
}
#[test]
fn test_strip_conditional_keywords() {
let mut schema = json!({
"type": "object",
"if": { "properties": { "kind": { "const": "a" } } },
"then": { "required": ["extra"] },
"else": { "required": [] },
"properties": { "kind": { "type": "string" } }
});
strip_conditional_keywords(&mut schema);
assert!(schema.get("if").is_none());
assert!(schema.get("then").is_none());
assert!(schema.get("else").is_none());
assert!(schema.get("properties").is_some());
}
#[test]
fn test_strip_conditional_keywords_nested() {
let mut schema = json!({
"type": "object",
"properties": {
"child": {
"type": "object",
"if": { "const": true },
"then": { "type": "string" }
}
}
});
strip_conditional_keywords(&mut schema);
assert!(schema["properties"]["child"].get("if").is_none());
assert!(schema["properties"]["child"].get("then").is_none());
}
#[test]
fn test_add_implicit_object_type() {
let mut schema = json!({
"properties": {
"name": { "type": "string" }
}
});
add_implicit_object_type(&mut schema);
assert_eq!(schema["type"], "object");
}
#[test]
fn test_add_implicit_object_type_no_op_when_type_present() {
let mut schema = json!({
"type": "object",
"properties": {
"name": { "type": "string" }
}
});
let expected = schema.clone();
add_implicit_object_type(&mut schema);
assert_eq!(schema, expected);
}
#[test]
fn test_add_implicit_object_type_nested() {
let mut schema = json!({
"type": "object",
"properties": {
"nested": {
"properties": {
"field": { "type": "number" }
}
}
}
});
add_implicit_object_type(&mut schema);
assert_eq!(schema["properties"]["nested"]["type"], "object");
}
#[test]
fn test_convert_const_to_enum() {
let mut schema = json!({
"type": "string",
"const": "fixed"
});
convert_const_to_enum(&mut schema);
assert!(schema.get("const").is_none());
assert_eq!(schema["enum"], json!(["fixed"]));
}
#[test]
fn test_convert_const_to_enum_null() {
let mut schema = json!({
"const": null
});
convert_const_to_enum(&mut schema);
assert!(schema.get("const").is_none());
assert_eq!(schema["enum"], json!([null]));
}
#[test]
fn test_convert_const_to_enum_nested() {
let mut schema = json!({
"type": "object",
"properties": {
"status": {
"type": "string",
"const": "active"
}
}
});
convert_const_to_enum(&mut schema);
assert_eq!(schema["properties"]["status"]["enum"], json!(["active"]));
}
#[test]
fn test_strip_unsupported_formats_removes_unsupported() {
let mut schema = json!({
"type": "string",
"format": "hostname"
});
strip_unsupported_formats(&mut schema, &["date-time", "email"]);
assert!(schema.get("format").is_none());
}
#[test]
fn test_strip_unsupported_formats_keeps_allowed() {
let mut schema = json!({
"type": "string",
"format": "email"
});
strip_unsupported_formats(&mut schema, &["date-time", "email"]);
assert_eq!(schema["format"], "email");
}
#[test]
fn test_strip_unsupported_formats_nested() {
let mut schema = json!({
"type": "object",
"properties": {
"created": { "type": "string", "format": "date-time" },
"hostname": { "type": "string", "format": "hostname" }
}
});
strip_unsupported_formats(&mut schema, &["date-time"]);
assert_eq!(schema["properties"]["created"]["format"], "date-time");
assert!(schema["properties"]["hostname"].get("format").is_none());
}
#[test]
fn test_truncate_tool_name_short() {
let result = truncate_tool_name("short_name", 64);
assert_eq!(result, "short_name");
assert!(matches!(result, Cow::Borrowed(_)));
}
#[test]
fn test_truncate_tool_name_exact_boundary() {
let name = "a".repeat(64);
let result = truncate_tool_name(&name, 64);
assert_eq!(result.len(), 64);
assert!(matches!(result, Cow::Borrowed(_)));
}
#[test]
fn test_truncate_tool_name_over_limit() {
let name = "a".repeat(100);
let result = truncate_tool_name(&name, 64);
assert_eq!(result.len(), 64);
assert!(matches!(result, Cow::Owned(_)));
}
#[test]
fn test_truncate_tool_name_multibyte_boundary() {
let name = "a".repeat(63) + "é"; let result = truncate_tool_name(&name, 64);
assert_eq!(result.len(), 63);
assert!(result.is_char_boundary(result.len()));
}
#[test]
fn test_truncate_tool_name_emoji() {
let name = "a".repeat(62) + "🎯"; let result = truncate_tool_name(&name, 64);
assert_eq!(result.len(), 62);
}
#[test]
fn test_truncate_tool_name_empty() {
let result = truncate_tool_name("", 64);
assert_eq!(result, "");
assert!(matches!(result, Cow::Borrowed(_)));
}
#[test]
fn test_strip_null_from_enum() {
let mut schema = json!({
"type": "string",
"enum": ["a", null, "b"]
});
strip_null_from_enum(&mut schema);
assert_eq!(schema["enum"], json!(["a", "b"]));
}
#[test]
fn test_strip_null_from_enum_all_null() {
let mut schema = json!({
"type": "string",
"enum": [null]
});
strip_null_from_enum(&mut schema);
assert!(schema.get("enum").is_none());
}
#[test]
fn test_strip_null_from_enum_no_null() {
let mut schema = json!({
"type": "string",
"enum": ["a", "b"]
});
let expected = schema.clone();
strip_null_from_enum(&mut schema);
assert_eq!(schema, expected);
}
#[test]
fn test_strip_null_from_enum_nested() {
let mut schema = json!({
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": ["active", null, "inactive"]
}
}
});
strip_null_from_enum(&mut schema);
assert_eq!(schema["properties"]["status"]["enum"], json!(["active", "inactive"]));
}
#[test]
fn test_recursion_into_any_of() {
let mut schema = json!({
"anyOf": [
{ "$schema": "draft-07", "type": "string" },
{ "$schema": "draft-07", "type": "number" }
]
});
strip_schema_keyword(&mut schema);
assert!(schema["anyOf"][0].get("$schema").is_none());
assert!(schema["anyOf"][1].get("$schema").is_none());
}
#[test]
fn test_recursion_into_all_of() {
let mut schema = json!({
"allOf": [
{ "properties": { "a": { "type": "string" } } },
{ "properties": { "b": { "type": "number" } } }
]
});
add_implicit_object_type(&mut schema);
assert_eq!(schema["allOf"][0]["type"], "object");
assert_eq!(schema["allOf"][1]["type"], "object");
}
#[test]
fn test_recursion_into_items() {
let mut schema = json!({
"type": "array",
"items": {
"$schema": "draft-07",
"type": "string",
"format": "hostname"
}
});
strip_schema_keyword(&mut schema);
strip_unsupported_formats(&mut schema, &["date-time"]);
assert!(schema["items"].get("$schema").is_none());
assert!(schema["items"].get("format").is_none());
}
#[test]
fn test_recursion_into_additional_properties() {
let mut schema = json!({
"type": "object",
"additionalProperties": {
"$schema": "draft-07",
"type": "string"
}
});
strip_schema_keyword(&mut schema);
assert!(schema["additionalProperties"].get("$schema").is_none());
}
#[test]
fn test_recursion_into_not() {
let mut schema = json!({
"not": {
"$schema": "draft-07",
"type": "null"
}
});
strip_schema_keyword(&mut schema);
assert!(schema["not"].get("$schema").is_none());
}
#[test]
fn test_deeply_nested_recursion() {
let mut schema = json!({
"type": "object",
"properties": {
"level1": {
"type": "object",
"properties": {
"level2": {
"type": "object",
"properties": {
"level3": {
"$schema": "draft-07",
"type": "string",
"const": "deep"
}
}
}
}
}
}
});
strip_schema_keyword(&mut schema);
convert_const_to_enum(&mut schema);
let deep = &schema["properties"]["level1"]["properties"]["level2"]["properties"]["level3"];
assert!(deep.get("$schema").is_none());
assert_eq!(deep["enum"], json!(["deep"]));
}
#[test]
fn test_resolve_refs_simple_definitions() {
let mut defs = Map::new();
defs.insert(
"Address".to_string(),
json!({"type": "object", "properties": {"street": {"type": "string"}}}),
);
let mut schema = json!({
"type": "object",
"properties": {
"home": { "$ref": "#/definitions/Address" }
}
});
resolve_refs(&mut schema, &defs, 0);
assert_eq!(schema["properties"]["home"]["type"], "object");
assert!(schema["properties"]["home"].get("$ref").is_none());
assert_eq!(schema["properties"]["home"]["properties"]["street"]["type"], "string");
}
#[test]
fn test_resolve_refs_simple_defs_format() {
let mut defs = Map::new();
defs.insert("Name".to_string(), json!({"type": "string", "minLength": 1}));
let mut schema = json!({
"type": "object",
"properties": {
"name": { "$ref": "#/$defs/Name" }
}
});
resolve_refs(&mut schema, &defs, 0);
assert_eq!(schema["properties"]["name"]["type"], "string");
assert_eq!(schema["properties"]["name"]["minLength"], 1);
assert!(schema["properties"]["name"].get("$ref").is_none());
}
#[test]
fn test_resolve_refs_nested_refs() {
let mut defs = Map::new();
defs.insert("Inner".to_string(), json!({"type": "string"}));
defs.insert(
"Outer".to_string(),
json!({
"type": "object",
"properties": {
"value": { "$ref": "#/definitions/Inner" }
}
}),
);
let mut schema = json!({
"type": "object",
"properties": {
"wrapper": { "$ref": "#/definitions/Outer" }
}
});
resolve_refs(&mut schema, &defs, 0);
assert_eq!(schema["properties"]["wrapper"]["type"], "object");
assert_eq!(schema["properties"]["wrapper"]["properties"]["value"]["type"], "string");
assert!(schema["properties"]["wrapper"]["properties"]["value"].get("$ref").is_none());
}
#[test]
fn test_resolve_refs_unresolvable_ref() {
let defs = Map::new();
let mut schema = json!({
"type": "object",
"properties": {
"missing": { "$ref": "#/definitions/DoesNotExist" }
}
});
resolve_refs(&mut schema, &defs, 0);
assert_eq!(schema["properties"]["missing"], json!({"type": "object"}));
}
#[test]
fn test_resolve_refs_unsupported_ref_format() {
let defs = Map::new();
let mut schema = json!({
"type": "object",
"properties": {
"external": { "$ref": "https://example.com/schema.json" }
}
});
resolve_refs(&mut schema, &defs, 0);
assert_eq!(schema["properties"]["external"], json!({"type": "object"}));
}
#[test]
fn test_resolve_refs_circular_self_reference() {
let mut defs = Map::new();
defs.insert(
"Node".to_string(),
json!({
"type": "object",
"properties": {
"child": { "$ref": "#/definitions/Node" }
}
}),
);
let mut schema = json!({ "$ref": "#/definitions/Node" });
resolve_refs(&mut schema, &defs, 0);
assert_eq!(schema["type"], "object");
let mut current = &schema;
let mut found_termination = false;
for _ in 0..15 {
if let Some(child) = current.get("properties").and_then(|p| p.get("child")) {
if child == &json!({"type": "object"}) {
found_termination = true;
break;
}
current = child;
} else {
found_termination = true;
break;
}
}
assert!(found_termination, "circular ref chain should terminate within depth limit");
}
#[test]
fn test_resolve_refs_mutual_circular_reference() {
let mut defs = Map::new();
defs.insert(
"A".to_string(),
json!({
"type": "object",
"properties": {
"b": { "$ref": "#/definitions/B" }
}
}),
);
defs.insert(
"B".to_string(),
json!({
"type": "object",
"properties": {
"a": { "$ref": "#/definitions/A" }
}
}),
);
let mut schema = json!({ "$ref": "#/definitions/A" });
resolve_refs(&mut schema, &defs, 0);
assert_eq!(schema["type"], "object");
}
#[test]
fn test_resolve_refs_depth_limit_exact() {
let mut defs = Map::new();
defs.insert("Foo".to_string(), json!({"type": "number"}));
let mut schema = json!({ "$ref": "#/definitions/Foo" });
resolve_refs(&mut schema, &defs, 11);
assert_eq!(schema, json!({"type": "object"}));
}
#[test]
fn test_resolve_refs_depth_limit_no_ref_passthrough() {
let defs = Map::new();
let mut schema = json!({"type": "string", "minLength": 5});
let expected = schema.clone();
resolve_refs(&mut schema, &defs, 11);
assert_eq!(schema, expected);
}
#[test]
fn test_resolve_refs_depth_10_still_resolves() {
let mut defs = Map::new();
defs.insert("Foo".to_string(), json!({"type": "number"}));
let mut schema = json!({ "$ref": "#/definitions/Foo" });
resolve_refs(&mut schema, &defs, 10);
assert_eq!(schema, json!({"type": "number"}));
}
#[test]
fn test_resolve_refs_in_array_items() {
let mut defs = Map::new();
defs.insert("Item".to_string(), json!({"type": "string"}));
let mut schema = json!({
"type": "array",
"items": { "$ref": "#/definitions/Item" }
});
resolve_refs(&mut schema, &defs, 0);
assert_eq!(schema["items"]["type"], "string");
assert!(schema["items"].get("$ref").is_none());
}
#[test]
fn test_resolve_refs_in_any_of() {
let mut defs = Map::new();
defs.insert("Str".to_string(), json!({"type": "string"}));
defs.insert("Num".to_string(), json!({"type": "number"}));
let mut schema = json!({
"anyOf": [
{ "$ref": "#/definitions/Str" },
{ "$ref": "#/$defs/Num" }
]
});
resolve_refs(&mut schema, &defs, 0);
assert_eq!(schema["anyOf"][0], json!({"type": "string"}));
assert_eq!(schema["anyOf"][1], json!({"type": "number"}));
}
#[test]
fn test_resolve_refs_no_ref_passthrough() {
let defs = Map::new();
let mut schema = json!({
"type": "object",
"properties": {
"name": { "type": "string" },
"age": { "type": "integer" }
}
});
let expected = schema.clone();
resolve_refs(&mut schema, &defs, 0);
assert_eq!(schema, expected);
}
#[test]
fn test_resolve_refs_both_definitions_and_defs() {
let mut defs = Map::new();
defs.insert("FromDefs".to_string(), json!({"type": "boolean"}));
defs.insert("FromDefinitions".to_string(), json!({"type": "integer"}));
let mut schema = json!({
"type": "object",
"properties": {
"a": { "$ref": "#/$defs/FromDefs" },
"b": { "$ref": "#/definitions/FromDefinitions" }
}
});
resolve_refs(&mut schema, &defs, 0);
assert_eq!(schema["properties"]["a"], json!({"type": "boolean"}));
assert_eq!(schema["properties"]["b"], json!({"type": "integer"}));
}
#[test]
fn test_collapse_combiners_any_of_picks_first_non_null() {
let mut schema = json!({
"anyOf": [
{"type": "null"},
{"type": "string", "minLength": 1}
]
});
collapse_combiners(&mut schema);
assert_eq!(schema["type"], "string");
assert_eq!(schema["minLength"], 1);
assert!(schema.get("anyOf").is_none());
}
#[test]
fn test_collapse_combiners_one_of_picks_first_non_null() {
let mut schema = json!({
"oneOf": [
{"type": "null"},
{"type": "integer", "minimum": 0}
]
});
collapse_combiners(&mut schema);
assert_eq!(schema["type"], "integer");
assert_eq!(schema["minimum"], 0);
assert!(schema.get("oneOf").is_none());
}
#[test]
fn test_collapse_combiners_all_null_uses_first() {
let mut schema = json!({
"anyOf": [
{"type": "null"},
{"type": "null"}
]
});
collapse_combiners(&mut schema);
assert_eq!(schema["type"], "null");
assert!(schema.get("anyOf").is_none());
}
#[test]
fn test_collapse_combiners_no_null() {
let mut schema = json!({
"anyOf": [
{"type": "string"},
{"type": "number"}
]
});
collapse_combiners(&mut schema);
assert_eq!(schema["type"], "string");
assert!(schema.get("anyOf").is_none());
}
#[test]
fn test_collapse_combiners_nested() {
let mut schema = json!({
"type": "object",
"properties": {
"field": {
"oneOf": [
{"type": "null"},
{"type": "boolean"}
]
}
}
});
collapse_combiners(&mut schema);
assert_eq!(schema["properties"]["field"]["type"], "boolean");
assert!(schema["properties"]["field"].get("oneOf").is_none());
}
#[test]
fn test_collapse_combiners_preserves_existing_fields() {
let mut schema = json!({
"description": "A nullable string",
"anyOf": [
{"type": "null"},
{"type": "string", "maxLength": 100}
]
});
collapse_combiners(&mut schema);
assert_eq!(schema["description"], "A nullable string");
assert_eq!(schema["type"], "string");
assert_eq!(schema["maxLength"], 100);
}
#[test]
fn test_merge_all_of_combines_properties() {
let mut schema = json!({
"allOf": [
{"type": "object", "properties": {"a": {"type": "string"}}},
{"properties": {"b": {"type": "number"}}}
]
});
merge_all_of(&mut schema);
assert!(schema.get("allOf").is_none());
assert_eq!(schema["properties"]["a"]["type"], "string");
assert_eq!(schema["properties"]["b"]["type"], "number");
}
#[test]
fn test_merge_all_of_combines_required() {
let mut schema = json!({
"allOf": [
{"required": ["a", "b"]},
{"required": ["b", "c"]}
]
});
merge_all_of(&mut schema);
let required = schema["required"].as_array().unwrap();
assert!(required.contains(&json!("a")));
assert!(required.contains(&json!("b")));
assert!(required.contains(&json!("c")));
assert_eq!(required.len(), 3);
}
#[test]
fn test_merge_all_of_conflicting_type_prefers_object() {
let mut schema = json!({
"allOf": [
{"type": "string"},
{"type": "number"}
]
});
merge_all_of(&mut schema);
assert_eq!(schema["type"], "object");
}
#[test]
fn test_merge_all_of_same_type_no_conflict() {
let mut schema = json!({
"allOf": [
{"type": "object", "properties": {"a": {"type": "string"}}},
{"type": "object", "properties": {"b": {"type": "number"}}}
]
});
merge_all_of(&mut schema);
assert_eq!(schema["type"], "object");
}
#[test]
fn test_merge_all_of_nested() {
let mut schema = json!({
"type": "object",
"properties": {
"nested": {
"allOf": [
{"properties": {"x": {"type": "integer"}}},
{"properties": {"y": {"type": "integer"}}}
]
}
}
});
merge_all_of(&mut schema);
assert!(schema["properties"]["nested"].get("allOf").is_none());
assert_eq!(schema["properties"]["nested"]["properties"]["x"]["type"], "integer");
assert_eq!(schema["properties"]["nested"]["properties"]["y"]["type"], "integer");
}
#[test]
fn test_merge_all_of_other_fields() {
let mut schema = json!({
"allOf": [
{"type": "object", "description": "First"},
{"title": "Second"}
]
});
merge_all_of(&mut schema);
assert_eq!(schema["description"], "First");
assert_eq!(schema["title"], "Second");
}
#[test]
fn test_collapse_type_arrays_string_null() {
let mut schema = json!({"type": ["string", "null"]});
collapse_type_arrays(&mut schema);
assert_eq!(schema["type"], "string");
}
#[test]
fn test_collapse_type_arrays_null_first() {
let mut schema = json!({"type": ["null", "integer"]});
collapse_type_arrays(&mut schema);
assert_eq!(schema["type"], "integer");
}
#[test]
fn test_collapse_type_arrays_all_null() {
let mut schema = json!({"type": ["null"]});
collapse_type_arrays(&mut schema);
assert_eq!(schema["type"], "null");
}
#[test]
fn test_collapse_type_arrays_single_non_null() {
let mut schema = json!({"type": ["boolean"]});
collapse_type_arrays(&mut schema);
assert_eq!(schema["type"], "boolean");
}
#[test]
fn test_collapse_type_arrays_already_string() {
let mut schema = json!({"type": "string"});
let expected = schema.clone();
collapse_type_arrays(&mut schema);
assert_eq!(schema, expected);
}
#[test]
fn test_collapse_type_arrays_nested() {
let mut schema = json!({
"type": "object",
"properties": {
"field": {"type": ["number", "null"]}
}
});
collapse_type_arrays(&mut schema);
assert_eq!(schema["properties"]["field"]["type"], "number");
}
#[test]
fn test_collapse_type_arrays_multiple_non_null() {
let mut schema = json!({"type": ["string", "number", "null"]});
collapse_type_arrays(&mut schema);
assert_eq!(schema["type"], "string");
}
#[test]
fn test_enforce_nesting_depth_within_limit() {
let mut schema = json!({
"type": "object",
"properties": {
"name": {"type": "string"}
}
});
let expected = schema.clone();
enforce_nesting_depth(&mut schema, 5, 0);
assert_eq!(schema, expected);
}
#[test]
fn test_enforce_nesting_depth_at_limit() {
let mut schema = json!({
"type": "object",
"properties": {
"deep": {
"type": "object",
"properties": {
"deeper": {"type": "string"}
}
}
}
});
enforce_nesting_depth(&mut schema, 1, 0);
assert_eq!(schema["properties"]["deep"], json!({"type": "object"}));
}
#[test]
fn test_enforce_nesting_depth_exceeds_limit() {
let mut schema = json!({
"type": "object",
"properties": {
"level1": {
"type": "object",
"properties": {
"level2": {
"type": "object",
"properties": {
"level3": {"type": "string"}
}
}
}
}
}
});
enforce_nesting_depth(&mut schema, 2, 0);
assert_eq!(
schema["properties"]["level1"]["properties"]["level2"],
json!({"type": "object"})
);
}
#[test]
fn test_enforce_nesting_depth_non_object_not_counted() {
let mut schema = json!({
"type": "object",
"properties": {
"arr": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {"type": "string"}
}
}
}
}
});
enforce_nesting_depth(&mut schema, 2, 0);
assert_eq!(schema["properties"]["arr"]["items"]["properties"]["name"]["type"], "string");
}
#[test]
fn test_enforce_nesting_depth_gemini_5_levels() {
let mut schema = json!({
"type": "object",
"properties": {
"l1": {
"type": "object",
"properties": {
"l2": {
"type": "object",
"properties": {
"l3": {
"type": "object",
"properties": {
"l4": {
"type": "object",
"properties": {
"l5": {
"type": "object",
"properties": {
"deep": {"type": "string"}
}
}
}
}
}
}
}
}
}
}
}
});
enforce_nesting_depth(&mut schema, 5, 0);
assert_eq!(
schema["properties"]["l1"]["properties"]["l2"]["properties"]["l3"]["properties"]["l4"]
["properties"]["l5"],
json!({"type": "object"})
);
assert!(
schema["properties"]["l1"]["properties"]["l2"]["properties"]["l3"]["properties"]["l4"]
.get("properties")
.is_some()
);
}
#[test]
fn test_enforce_nesting_depth_zero_truncates_root_object() {
let mut schema = json!({
"type": "object",
"properties": {"a": {"type": "string"}}
});
enforce_nesting_depth(&mut schema, 0, 0);
assert_eq!(schema, json!({"type": "object"}));
}
#[test]
fn test_is_null_schema_true() {
assert!(is_null_schema(&json!({"type": "null"})));
}
#[test]
fn test_is_null_schema_false_for_string() {
assert!(!is_null_schema(&json!({"type": "string"})));
}
#[test]
fn test_is_null_schema_false_for_non_object() {
assert!(!is_null_schema(&json!("null")));
assert!(!is_null_schema(&Value::Null));
}
}