use serde_json::Value;
use std::borrow::Cow;
use crate::schema_utils;
pub trait SchemaAdapter: Send + Sync + std::fmt::Debug {
fn normalize_schema(&self, schema: Value) -> Value;
fn normalize_tool_name<'a>(&self, name: &'a str) -> Cow<'a, str> {
if name.len() <= 64 {
Cow::Borrowed(name)
} else {
let mut end = 64;
while end > 0 && !name.is_char_boundary(end) {
end -= 1;
}
Cow::Owned(name[..end].to_string())
}
}
fn empty_schema(&self) -> Value {
serde_json::json!({"type": "object", "properties": {}})
}
}
#[derive(Debug)]
pub struct GenericSchemaAdapter;
const GENERIC_ALLOWED_FORMATS: &[&str] =
&["date-time", "date", "time", "email", "uri", "uuid", "int32", "int64", "float", "double"];
impl SchemaAdapter for GenericSchemaAdapter {
fn normalize_schema(&self, mut schema: Value) -> Value {
schema_utils::strip_schema_keyword(&mut schema);
schema_utils::strip_conditional_keywords(&mut schema);
schema_utils::convert_const_to_enum(&mut schema);
schema_utils::add_implicit_object_type(&mut schema);
schema_utils::strip_unsupported_formats(&mut schema, GENERIC_ALLOWED_FORMATS);
schema
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_generic_adapter_strips_schema_keyword() {
let adapter = GenericSchemaAdapter;
let schema = json!({
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": { "name": { "type": "string" } }
});
let result = adapter.normalize_schema(schema);
assert!(result.get("$schema").is_none());
assert_eq!(result["type"], "object");
}
#[test]
fn test_generic_adapter_strips_conditional_keywords() {
let adapter = GenericSchemaAdapter;
let schema = json!({
"type": "object",
"if": { "properties": { "kind": { "const": "a" } } },
"then": { "required": ["extra"] },
"else": { "required": [] }
});
let result = adapter.normalize_schema(schema);
assert!(result.get("if").is_none());
assert!(result.get("then").is_none());
assert!(result.get("else").is_none());
}
#[test]
fn test_generic_adapter_converts_const_to_enum() {
let adapter = GenericSchemaAdapter;
let schema = json!({
"type": "string",
"const": "fixed_value"
});
let result = adapter.normalize_schema(schema);
assert!(result.get("const").is_none());
assert_eq!(result["enum"], json!(["fixed_value"]));
}
#[test]
fn test_generic_adapter_adds_implicit_object_type() {
let adapter = GenericSchemaAdapter;
let schema = json!({
"properties": {
"name": { "type": "string" }
}
});
let result = adapter.normalize_schema(schema);
assert_eq!(result["type"], "object");
}
#[test]
fn test_generic_adapter_strips_unsupported_formats() {
let adapter = GenericSchemaAdapter;
let schema = json!({
"type": "object",
"properties": {
"created": { "type": "string", "format": "date-time" },
"hostname": { "type": "string", "format": "hostname" },
"email": { "type": "string", "format": "email" }
}
});
let result = adapter.normalize_schema(schema);
assert_eq!(result["properties"]["created"]["format"], "date-time");
assert!(result["properties"]["hostname"].get("format").is_none());
assert_eq!(result["properties"]["email"]["format"], "email");
}
#[test]
fn test_generic_adapter_preserves_allowed_formats() {
let adapter = GenericSchemaAdapter;
for format in GENERIC_ALLOWED_FORMATS {
let schema = json!({ "type": "string", "format": format });
let result = adapter.normalize_schema(schema);
assert_eq!(result["format"], *format, "format '{format}' should be preserved");
}
}
#[test]
fn test_generic_adapter_all_transforms_combined() {
let adapter = GenericSchemaAdapter;
let schema = json!({
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"status": { "type": "string", "const": "active" },
"host": { "type": "string", "format": "hostname" },
"created": { "type": "string", "format": "date-time" }
},
"if": { "properties": { "status": { "const": "active" } } },
"then": { "required": ["host"] }
});
let result = adapter.normalize_schema(schema);
assert!(result.get("$schema").is_none());
assert!(result.get("if").is_none());
assert!(result.get("then").is_none());
assert_eq!(result["type"], "object");
assert!(result["properties"]["status"].get("const").is_none());
assert_eq!(result["properties"]["status"]["enum"], json!(["active"]));
assert!(result["properties"]["host"].get("format").is_none());
assert_eq!(result["properties"]["created"]["format"], "date-time");
}
#[test]
fn test_generic_adapter_nested_transforms() {
let adapter = GenericSchemaAdapter;
let schema = json!({
"type": "object",
"properties": {
"nested": {
"$schema": "draft-07",
"properties": {
"deep": {
"type": "string",
"const": "value",
"format": "ipv4"
}
},
"if": { "const": true },
"then": { "type": "string" }
}
}
});
let result = adapter.normalize_schema(schema);
let nested = &result["properties"]["nested"];
assert!(nested.get("$schema").is_none());
assert!(nested.get("if").is_none());
assert!(nested.get("then").is_none());
assert_eq!(nested["type"], "object");
assert_eq!(nested["properties"]["deep"]["enum"], json!(["value"]));
assert!(nested["properties"]["deep"].get("format").is_none());
}
#[test]
fn test_generic_adapter_idempotent() {
let adapter = GenericSchemaAdapter;
let schema = json!({
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"name": { "type": "string", "const": "test", "format": "hostname" }
},
"if": { "const": true },
"then": { "required": ["name"] }
});
let first = adapter.normalize_schema(schema);
let second = adapter.normalize_schema(first.clone());
assert_eq!(first, second);
}
#[test]
fn test_generic_adapter_empty_schema_passthrough() {
let adapter = GenericSchemaAdapter;
let schema = json!({});
let result = adapter.normalize_schema(schema);
assert_eq!(result, json!({}));
}
#[test]
fn test_generic_adapter_preserves_refs_and_combiners() {
let adapter = GenericSchemaAdapter;
let schema = json!({
"type": "object",
"$ref": "#/definitions/Foo",
"anyOf": [{ "type": "string" }, { "type": "number" }],
"oneOf": [{ "type": "boolean" }],
"allOf": [{ "required": ["a"] }],
"additionalProperties": false
});
let result = adapter.normalize_schema(schema);
assert!(result.get("$ref").is_some());
assert!(result.get("anyOf").is_some());
assert!(result.get("oneOf").is_some());
assert!(result.get("allOf").is_some());
assert!(result.get("additionalProperties").is_some());
}
}