| use serde_json::Value; |
|
|
| |
| |
| |
| |
| |
| |
| |
| pub fn clean_json_schema(value: &mut Value) { |
| |
| if let Value::Object(map) = value { |
| let mut defs = serde_json::Map::new(); |
| |
| if let Some(Value::Object(d)) = map.remove("$defs") { |
| defs.extend(d); |
| } |
| if let Some(Value::Object(d)) = map.remove("definitions") { |
| defs.extend(d); |
| } |
|
|
| if !defs.is_empty() { |
| |
| flatten_refs(map, &defs); |
| } |
| } |
|
|
| |
| clean_json_schema_recursive(value); |
| } |
|
|
| |
| fn flatten_refs(map: &mut serde_json::Map<String, Value>, defs: &serde_json::Map<String, Value>) { |
| |
| if let Some(Value::String(ref_path)) = map.remove("$ref") { |
| |
| let ref_name = ref_path.split('/').last().unwrap_or(&ref_path); |
| |
| if let Some(def_schema) = defs.get(ref_name) { |
| |
| if let Value::Object(def_map) = def_schema { |
| for (k, v) in def_map { |
| |
| |
| map.entry(k.clone()).or_insert_with(|| v.clone()); |
| } |
| |
| |
| |
| flatten_refs(map, defs); |
| } |
| } |
| } |
|
|
| |
| for (_, v) in map.iter_mut() { |
| if let Value::Object(child_map) = v { |
| flatten_refs(child_map, defs); |
| } else if let Value::Array(arr) = v { |
| for item in arr { |
| if let Value::Object(item_map) = item { |
| flatten_refs(item_map, defs); |
| } |
| } |
| } |
| } |
| } |
|
|
| fn clean_json_schema_recursive(value: &mut Value) { |
| match value { |
| Value::Object(map) => { |
| |
| |
| for v in map.values_mut() { |
| clean_json_schema_recursive(v); |
| } |
|
|
| |
| let mut constraints = Vec::new(); |
| |
| |
| let validation_fields = [ |
| ("pattern", "pattern"), |
| ("minLength", "minLen"), ("maxLength", "maxLen"), |
| ("minimum", "min"), ("maximum", "max"), |
| ("minItems", "minItems"), ("maxItems", "maxItems"), |
| ("exclusiveMinimum", "exclMin"), ("exclusiveMaximum", "exclMax"), |
| ("multipleOf", "multipleOf"), |
| ("format", "format"), |
| ]; |
|
|
| for (field, label) in validation_fields { |
| if let Some(val) = map.remove(field) { |
| |
| if val.is_string() || val.is_number() || val.is_boolean() { |
| constraints.push(format!("{}: {}", label, val)); |
| } |
| } |
| } |
|
|
| |
| if !constraints.is_empty() { |
| let suffix = format!(" [Constraint: {}]", constraints.join(", ")); |
| let desc_val = map.entry("description".to_string()).or_insert_with(|| Value::String("".to_string())); |
| if let Value::String(s) = desc_val { |
| s.push_str(&suffix); |
| } |
| } |
|
|
| |
| let hard_remove_fields = [ |
| "$schema", |
| "additionalProperties", |
| "enumCaseInsensitive", |
| "enumNormalizeWhitespace", |
| "uniqueItems", |
| "default", |
| "const", |
| "examples", |
| "propertyNames", |
| "anyOf", "oneOf", "allOf", "not", |
| "if", "then", "else", |
| "dependencies", "dependentSchemas", "dependentRequired", |
| "cache_control", |
| ]; |
| for field in hard_remove_fields { |
| map.remove(field); |
| } |
|
|
| |
| if let Some(type_val) = map.get_mut("type") { |
| match type_val { |
| Value::String(s) => { |
| *type_val = Value::String(s.to_lowercase()); |
| } |
| Value::Array(arr) => { |
| let mut selected_type = "string".to_string(); |
| for item in arr { |
| if let Value::String(s) = item { |
| if s != "null" { |
| selected_type = s.to_lowercase(); |
| break; |
| } |
| } |
| } |
| *type_val = Value::String(selected_type); |
| } |
| _ => {} |
| } |
| } |
| } |
| Value::Array(arr) => { |
| for v in arr.iter_mut() { |
| clean_json_schema_recursive(v); |
| } |
| } |
| _ => {} |
| } |
| } |
|
|
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use serde_json::json; |
|
|
| #[test] |
| fn test_clean_json_schema_draft_2020_12() { |
| let mut schema = json!({ |
| "$schema": "http://json-schema.org/draft-07/schema#", |
| "type": "object", |
| "properties": { |
| "location": { |
| "type": "string", |
| "minLength": 1, |
| "format": "city" |
| }, |
| |
| "pattern": { |
| "type": "object", |
| "properties": { |
| "regex": { "type": "string", "pattern": "^[a-z]+$" } |
| } |
| }, |
| "unit": { |
| "type": ["string", "null"], |
| "default": "celsius" |
| } |
| }, |
| "required": ["location"] |
| }); |
|
|
| clean_json_schema(&mut schema); |
|
|
| |
| assert_eq!(schema["type"], "object"); |
| assert_eq!(schema["properties"]["location"]["type"], "string"); |
|
|
| |
| assert!(schema["properties"]["location"].get("minLength").is_none()); |
| assert!(schema["properties"]["location"]["description"].as_str().unwrap().contains("minLen: 1")); |
|
|
| |
| assert!(schema["properties"].get("pattern").is_some()); |
| assert_eq!(schema["properties"]["pattern"]["type"], "object"); |
|
|
| |
| assert!(schema["properties"]["pattern"]["properties"]["regex"].get("pattern").is_none()); |
| assert!(schema["properties"]["pattern"]["properties"]["regex"]["description"].as_str().unwrap().contains("pattern: ^[a-z]+$")); |
|
|
| |
| assert_eq!(schema["properties"]["unit"]["type"], "string"); |
| |
| |
| assert!(schema.get("$schema").is_none()); |
| } |
|
|
| #[test] |
| fn test_type_fallback() { |
| |
| let mut s1 = json!({"type": ["string", "null"]}); |
| clean_json_schema(&mut s1); |
| assert_eq!(s1["type"], "string"); |
|
|
| |
| let mut s2 = json!({"type": ["integer", "null"]}); |
| clean_json_schema(&mut s2); |
| assert_eq!(s2["type"], "integer"); |
| } |
|
|
| #[test] |
| fn test_flatten_refs() { |
| let mut schema = json!({ |
| "$defs": { |
| "Address": { |
| "type": "object", |
| "properties": { |
| "city": { "type": "string" } |
| } |
| } |
| }, |
| "properties": { |
| "home": { "$ref": "#/$defs/Address" } |
| } |
| }); |
|
|
| clean_json_schema(&mut schema); |
|
|
| |
| assert_eq!(schema["properties"]["home"]["type"], "object"); |
| assert_eq!(schema["properties"]["home"]["properties"]["city"]["type"], "string"); |
| } |
| } |
|
|