#[cfg(feature = "schema-generation")]
use serde_json::{json, Value};
#[cfg(feature = "schema-generation")]
#[derive(Debug, Clone)]
pub struct NormalizerConfig {
pub max_inline_depth: usize,
pub max_inline_size: usize,
pub keep_definitions: bool,
pub remove_metadata: bool,
}
impl Default for NormalizerConfig {
fn default() -> Self {
Self {
max_inline_depth: 10,
max_inline_size: 100_000, keep_definitions: false,
remove_metadata: true,
}
}
}
#[cfg(feature = "schema-generation")]
pub fn normalize_schema(schema: Value) -> Value {
normalize_schema_with_config(schema, &NormalizerConfig::default())
}
#[cfg(feature = "schema-generation")]
pub fn normalize_schema_with_config(mut schema: Value, config: &NormalizerConfig) -> Value {
let schema_size = serde_json::to_string(&schema).unwrap_or_default().len();
if schema_size > config.max_inline_size {
if config.remove_metadata {
if let Some(obj) = schema.as_object_mut() {
obj.remove("$schema");
obj.remove("$id");
}
}
return schema;
}
let definitions = if config.keep_definitions {
schema
.as_object()
.and_then(|obj| obj.get("definitions").or_else(|| obj.get("$defs")))
.cloned()
} else {
schema
.as_object_mut()
.and_then(|obj| obj.remove("definitions"))
.or_else(|| schema.as_object_mut().and_then(|obj| obj.remove("$defs")))
};
let defs_clone = definitions.clone();
if let Some(defs) = definitions {
let mut context = InlineContext {
definitions: &defs,
current_depth: 0,
max_depth: config.max_inline_depth,
current_size: schema_size,
max_size: config.max_inline_size,
};
inline_refs_with_context(&mut schema, &mut context);
if config.keep_definitions {
if let Some(obj) = schema.as_object_mut() {
obj.insert("definitions".to_string(), defs);
}
}
}
if config.remove_metadata {
if let Some(obj) = schema.as_object_mut() {
obj.remove("$schema");
obj.remove("$id");
if !config.keep_definitions {
if let Some(Value::String(ref_str)) = obj.get("$ref") {
if ref_str.starts_with("#/definitions/") || ref_str.starts_with("#/$defs/") {
let def_name = ref_str.rsplit('/').next().unwrap_or("");
if let Some(defs) = defs_clone.as_ref() {
if let Some(def_value) = defs.get(def_name) {
return def_value.clone();
}
}
}
}
}
}
}
schema
}
#[cfg(feature = "schema-generation")]
struct InlineContext<'a> {
definitions: &'a Value,
current_depth: usize,
max_depth: usize,
current_size: usize,
max_size: usize,
}
#[cfg(feature = "schema-generation")]
fn inline_refs_with_context(value: &mut Value, context: &mut InlineContext<'_>) {
if context.current_depth >= context.max_depth {
return;
}
let value_size = serde_json::to_string(&value).unwrap_or_default().len();
if context.current_size + value_size > context.max_size {
return;
}
context.current_depth += 1;
context.current_size += value_size;
match value {
Value::Object(map) => {
if let Some(Value::String(ref_str)) = map.get("$ref") {
if ref_str.starts_with("#/definitions/") || ref_str.starts_with("#/$defs/") {
let def_name = ref_str.rsplit('/').next().unwrap_or("");
if let Some(def_value) = context.definitions.get(def_name) {
let def_size = serde_json::to_string(&def_value).unwrap_or_default().len();
if context.current_size + def_size <= context.max_size {
if let Some(def_obj) = def_value.as_object() {
map.remove("$ref");
for (key, val) in def_obj {
if !map.contains_key(key) {
map.insert(key.clone(), val.clone());
}
}
}
}
}
}
}
for (_key, val) in map.iter_mut() {
inline_refs_with_context(val, context);
}
},
Value::Array(arr) => {
for item in arr.iter_mut() {
inline_refs_with_context(item, context);
}
},
_ => {},
}
context.current_depth -= 1;
}
#[cfg(feature = "schema-generation")]
#[allow(dead_code)]
fn inline_refs(value: &mut Value, definitions: &Value) {
match value {
Value::Object(map) => {
if let Some(Value::String(ref_str)) = map.get("$ref") {
if ref_str.starts_with("#/definitions/") || ref_str.starts_with("#/$defs/") {
let def_name = ref_str.rsplit('/').next().unwrap_or("");
if let Some(def_value) = definitions.get(def_name) {
if let Some(def_obj) = def_value.as_object() {
map.remove("$ref");
for (key, val) in def_obj {
if !map.contains_key(key) {
map.insert(key.clone(), val.clone());
}
}
}
}
}
}
for (_key, val) in map.iter_mut() {
inline_refs(val, definitions);
}
},
Value::Array(arr) => {
for item in arr.iter_mut() {
inline_refs(item, definitions);
}
},
_ => {},
}
}
#[cfg(feature = "schema-generation")]
pub fn simple_schema(type_name: &str, description: Option<&str>) -> Value {
let mut schema = json!({
"type": match type_name {
"string" | "String" => "string",
"number" | "f64" | "f32" => "number",
"integer" | "i32" | "i64" | "u32" | "u64" | "usize" | "isize" => "integer",
"boolean" | "bool" => "boolean",
"array" | "Vec" => "array",
"object" | "HashMap" | "BTreeMap" => "object",
_ => "string", }
});
if let Some(desc) = description {
schema["description"] = json!(desc);
}
schema
}
#[cfg(feature = "schema-generation")]
pub fn add_constraints(mut schema: Value, constraints: SchemaConstraints) -> Value {
if let Some(obj) = schema.as_object_mut() {
if let Some(min_length) = constraints.min_length {
obj.insert("minLength".to_string(), json!(min_length));
}
if let Some(max_length) = constraints.max_length {
obj.insert("maxLength".to_string(), json!(max_length));
}
if let Some(pattern) = constraints.pattern {
obj.insert("pattern".to_string(), json!(pattern));
}
if let Some(minimum) = constraints.minimum {
obj.insert("minimum".to_string(), json!(minimum));
}
if let Some(maximum) = constraints.maximum {
obj.insert("maximum".to_string(), json!(maximum));
}
if let Some(exclusive_minimum) = constraints.exclusive_minimum {
obj.insert("exclusiveMinimum".to_string(), json!(exclusive_minimum));
}
if let Some(exclusive_maximum) = constraints.exclusive_maximum {
obj.insert("exclusiveMaximum".to_string(), json!(exclusive_maximum));
}
if let Some(min_items) = constraints.min_items {
obj.insert("minItems".to_string(), json!(min_items));
}
if let Some(max_items) = constraints.max_items {
obj.insert("maxItems".to_string(), json!(max_items));
}
if let Some(unique_items) = constraints.unique_items {
obj.insert("uniqueItems".to_string(), json!(unique_items));
}
if let Some(enum_values) = constraints.enum_values {
obj.insert("enum".to_string(), json!(enum_values));
}
}
schema
}
#[cfg(feature = "schema-generation")]
#[derive(Debug, Default)]
pub struct SchemaConstraints {
pub min_length: Option<usize>,
pub max_length: Option<usize>,
pub pattern: Option<String>,
pub minimum: Option<f64>,
pub maximum: Option<f64>,
pub exclusive_minimum: Option<f64>,
pub exclusive_maximum: Option<f64>,
pub min_items: Option<usize>,
pub max_items: Option<usize>,
pub unique_items: Option<bool>,
pub enum_values: Option<Vec<Value>>,
}
#[cfg(all(test, feature = "schema-generation"))]
mod tests {
use super::*;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
struct TestStruct {
name: String,
age: Option<u32>,
address: Address,
}
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
struct Address {
street: String,
city: String,
}
#[test]
fn test_normalize_schema() {
let schema = schemars::schema_for!(TestStruct);
let json_schema = serde_json::to_value(schema).unwrap();
let normalized = normalize_schema(json_schema);
assert!(!normalized.as_object().unwrap().contains_key("$schema"));
assert!(!normalized.as_object().unwrap().contains_key("definitions"));
let props = normalized["properties"].as_object().unwrap();
assert!(props.contains_key("name"));
assert!(props.contains_key("age"));
assert!(props.contains_key("address"));
let address_props = props["address"]["properties"].as_object().unwrap();
assert!(address_props.contains_key("street"));
assert!(address_props.contains_key("city"));
}
#[test]
fn test_simple_schema() {
let string_schema = simple_schema("string", Some("A test string"));
assert_eq!(string_schema["type"], "string");
assert_eq!(string_schema["description"], "A test string");
let number_schema = simple_schema("f64", None);
assert_eq!(number_schema["type"], "number");
let int_schema = simple_schema("i32", None);
assert_eq!(int_schema["type"], "integer");
}
#[test]
fn test_add_constraints() {
let base_schema = json!({"type": "string"});
let constraints = SchemaConstraints {
min_length: Some(5),
max_length: Some(20),
pattern: Some(r"^\w+$".to_string()),
..Default::default()
};
let constrained = add_constraints(base_schema, constraints);
assert_eq!(constrained["minLength"], 5);
assert_eq!(constrained["maxLength"], 20);
assert_eq!(constrained["pattern"], r"^\w+$");
}
}