use serde_json::Value;
use crate::message::CommandSchema;
pub fn normalize_schema(mut v: Value) -> Value {
normalize_in_place(&mut v);
v
}
pub(crate) fn normalize_command_schema(cs: CommandSchema) -> CommandSchema {
CommandSchema {
request: cs.request.map(normalize_schema),
response: cs.response.map(normalize_schema),
}
}
fn normalize_in_place(v: &mut Value) {
match v {
Value::Object(map) => {
map.remove("title");
map.remove("$schema");
let is_string = matches!(map.get("type"), Some(Value::String(t)) if t == "string");
if !is_string {
map.remove("format");
}
if matches!(map.get("type"), Some(Value::String(t)) if t == "object")
&& !map.contains_key("additionalProperties")
{
map.insert("additionalProperties".into(), Value::Bool(false));
}
for (_, child) in map.iter_mut() {
normalize_in_place(child);
}
}
Value::Array(arr) => {
for child in arr.iter_mut() {
normalize_in_place(child);
}
}
_ => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn strips_title_schema_and_numeric_format_and_adds_additional_properties_false() {
let input = json!({
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "BinaryOpReq",
"type": "object",
"properties": {
"a": { "title": "int64", "type": "integer", "format": "int64" },
"b": { "title": "int64", "type": "integer", "format": "int64" }
},
"required": ["a", "b"]
});
let got = normalize_schema(input);
assert_eq!(
got,
json!({
"type": "object",
"additionalProperties": false,
"properties": {
"a": { "type": "integer" },
"b": { "type": "integer" }
},
"required": ["a", "b"]
})
);
}
#[test]
fn preserves_format_on_string_schemas() {
let input = json!({
"type": "object",
"properties": {
"created": { "type": "string", "format": "date-time" },
"email": { "type": "string", "format": "email" },
"id": { "type": "string", "format": "uuid" }
},
"required": ["created", "email", "id"]
});
let got = normalize_schema(input);
assert_eq!(got["properties"]["created"]["format"], "date-time");
assert_eq!(got["properties"]["email"]["format"], "email");
assert_eq!(got["properties"]["id"]["format"], "uuid");
}
#[test]
fn strips_openapi_numeric_formats() {
for fmt in [
"int32", "int64", "uint8", "uint32", "uint64", "float", "double",
] {
let input = json!({ "type": "integer", "format": fmt });
let got = normalize_schema(input);
assert!(
got.get("format").is_none(),
"format `{fmt}` should have been stripped on non-string schema"
);
}
}
#[test]
fn leaves_existing_additional_properties_alone() {
let input = json!({
"type": "object",
"additionalProperties": true,
"properties": { "x": { "type": "string" } }
});
let got = normalize_schema(input);
assert_eq!(got["additionalProperties"], Value::Bool(true));
}
#[test]
fn normalizes_non_object_root_schemas() {
let input = json!({
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "String",
"type": "string"
});
let got = normalize_schema(input);
assert_eq!(got, json!({ "type": "string" }));
}
#[test]
fn recurses_into_definitions_and_oneof() {
let input = json!({
"title": "Outer",
"type": "object",
"properties": {
"choice": {
"oneOf": [
{ "title": "A", "type": "object", "properties": { "a": { "type": "integer" } } },
{ "title": "B", "type": "object", "properties": { "b": { "type": "integer" } } }
]
}
},
"$defs": {
"Inner": {
"title": "Inner",
"type": "object",
"properties": { "n": { "type": "integer" } }
}
}
});
let got = normalize_schema(input);
assert_eq!(got["additionalProperties"], Value::Bool(false));
assert!(got.get("title").is_none());
let branches = got["properties"]["choice"]["oneOf"].as_array().unwrap();
for b in branches {
assert!(b.get("title").is_none());
assert_eq!(b["additionalProperties"], Value::Bool(false));
}
assert!(got["$defs"]["Inner"].get("title").is_none());
assert_eq!(
got["$defs"]["Inner"]["additionalProperties"],
Value::Bool(false)
);
}
}