use serde_json::Value;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
pub enum SchemaTier {
#[default]
Full,
Medium,
Minimal,
}
impl SchemaTier {
pub fn parse(s: &str) -> Option<Self> {
match s {
"full" => Some(SchemaTier::Full),
"medium" => Some(SchemaTier::Medium),
"minimal" => Some(SchemaTier::Minimal),
_ => None,
}
}
pub fn as_str(&self) -> &'static str {
match self {
SchemaTier::Full => "full",
SchemaTier::Medium => "medium",
SchemaTier::Minimal => "minimal",
}
}
}
pub fn minify(description: &str, parameters: &Value, tier: SchemaTier) -> (String, Value) {
if tier == SchemaTier::Full {
return (description.to_string(), parameters.clone());
}
let budget = if tier == SchemaTier::Minimal { 1 } else { 2 };
let new_description = truncate_sentences(description, budget);
let mut new_parameters = parameters.clone();
minify_node(&mut new_parameters, tier);
let orig_bytes = description.len()
+ serde_json::to_string(parameters)
.map(|s| s.len())
.unwrap_or(0);
let new_bytes = new_description.len()
+ serde_json::to_string(&new_parameters)
.map(|s| s.len())
.unwrap_or(0);
if new_bytes >= orig_bytes {
(description.to_string(), parameters.clone())
} else {
(new_description, new_parameters)
}
}
fn minify_node(node: &mut Value, tier: SchemaTier) {
let Some(map) = node.as_object_mut() else {
return;
};
map.remove("examples");
map.remove("title");
if let Some(Value::String(d)) = map.get("description").cloned() {
let n = if tier == SchemaTier::Minimal { 1 } else { 2 };
map.insert(
"description".to_string(),
Value::String(truncate_sentences(&d, n)),
);
}
let required: Vec<String> = map
.get("required")
.and_then(Value::as_array)
.map(|a| {
a.iter()
.filter_map(|v| v.as_str().map(str::to_string))
.collect()
})
.unwrap_or_default();
if let Some(props) = map.get_mut("properties").and_then(|p| p.as_object_mut()) {
let keys: Vec<String> = props.keys().cloned().collect();
for key in keys {
let is_required = required.iter().any(|r| r == &key);
let Some(prop) = props.get_mut(&key) else {
continue;
};
if let Some(pm) = prop.as_object_mut() {
pm.remove("examples");
pm.remove("title");
if tier == SchemaTier::Minimal && !is_required {
pm.remove("description");
} else if let Some(Value::String(d)) = pm.get("description").cloned() {
pm.insert(
"description".to_string(),
Value::String(truncate_sentences(&d, 1)),
);
}
}
minify_node(prop, tier);
}
}
if let Some(items) = map.get_mut("items") {
match items {
Value::Array(items) => {
for item in items {
minify_node(item, tier);
}
}
_ => minify_node(items, tier),
}
}
for key in ["anyOf", "oneOf", "allOf"] {
if let Some(Value::Array(arr)) = map.get_mut(key) {
for item in arr {
minify_node(item, tier);
}
}
}
for key in ["if", "then", "else"] {
if let Some(v) = map.get_mut(key) {
minify_node(v, tier);
}
}
for key in ["$defs", "definitions", "patternProperties"] {
if let Some(Value::Object(sub)) = map.get_mut(key) {
for v in sub.values_mut() {
minify_node(v, tier);
}
}
}
}
const ABBREVIATIONS: &[&str] = &["e.g.", "i.e.", "etc.", "Mr.", "Mrs.", "Dr.", "vs.", "cf."];
fn truncate_sentences(s: &str, n: usize) -> String {
if n == 0 || s.is_empty() {
return s.to_string();
}
let bytes = s.as_bytes();
let mut count = 0;
for (i, &b) in bytes.iter().enumerate() {
if b == b'.' || b == b'!' || b == b'?' {
let boundary = i + 1 == bytes.len() || bytes[i + 1] == b' ' || bytes[i + 1] == b'\n';
if boundary {
if b == b'.' && ABBREVIATIONS.iter().any(|a| s[..=i].ends_with(a)) {
continue;
}
count += 1;
if count >= n {
return s[..=i].to_string();
}
}
}
}
s.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn full_tier_is_identity() {
let desc = "A very long description. With two sentences.";
let params = json!({"type":"object","properties":{"a":{"type":"string","description":"x","examples":["e"]}},"required":["a"]});
let (d, p) = minify(desc, ¶ms, SchemaTier::Full);
assert_eq!(d, desc);
assert_eq!(p, params);
}
#[test]
fn truncate_sentences_cuts_at_boundary() {
assert_eq!(truncate_sentences("One. Two. Three.", 1), "One.");
assert_eq!(truncate_sentences("One. Two. Three.", 2), "One. Two.");
assert_eq!(
truncate_sentences("No punctuation here", 1),
"No punctuation here"
);
assert_eq!(truncate_sentences("", 1), "");
}
#[test]
fn minimal_drops_optional_param_descriptions_keeps_required() {
let desc = "Does a thing. Has more detail. Even more.";
let params = json!({
"type": "object",
"properties": {
"req": {"type": "string", "description": "The required one. More detail here."},
"opt": {"type": "integer", "description": "The optional one. More detail here."}
},
"required": ["req"]
});
let (d, p) = minify(desc, ¶ms, SchemaTier::Minimal);
assert_eq!(d, "Does a thing.");
assert_eq!(p["properties"]["req"]["description"], "The required one.");
assert!(p["properties"]["opt"].get("description").is_none());
assert_eq!(p["properties"]["req"]["type"], "string");
assert_eq!(p["properties"]["opt"]["type"], "integer");
assert_eq!(p["required"], json!(["req"]));
}
#[test]
fn examples_and_title_stripped_at_every_tier_above_full() {
let params = json!({
"type": "object",
"title": "Top title",
"properties": {
"a": {"type": "string", "examples": ["x"], "title": "A title"}
},
"required": []
});
let (_, p) = minify("desc.", ¶ms, SchemaTier::Medium);
assert!(p.get("title").is_none());
assert!(p["properties"]["a"].get("examples").is_none());
assert!(p["properties"]["a"].get("title").is_none());
}
#[test]
fn byte_floor_never_grows_already_terse_schema() {
let desc = "Short.";
let params = json!({"type":"object","properties":{"a":{"type":"string"}},"required":["a"]});
let (d, p) = minify(desc, ¶ms, SchemaTier::Minimal);
assert_eq!(d, desc);
assert_eq!(p, params);
}
#[test]
fn nested_object_properties_get_required_aware_treatment_too() {
let params = json!({
"type": "object",
"properties": {
"outer": {
"type": "object",
"properties": {
"inner_req": {"type": "string", "description": "Inner required. More."},
"inner_opt": {"type": "string", "description": "Inner optional. More."}
},
"required": ["inner_req"]
}
},
"required": ["outer"]
});
let (_, p) = minify("desc. more.", ¶ms, SchemaTier::Minimal);
let outer = &p["properties"]["outer"];
assert_eq!(
outer["properties"]["inner_req"]["description"],
"Inner required."
);
assert!(outer["properties"]["inner_opt"]
.get("description")
.is_none());
assert_eq!(outer["properties"]["inner_req"]["type"], "string");
assert_eq!(outer["properties"]["inner_opt"]["type"], "string");
}
#[test]
fn array_items_are_recursed_into() {
let params = json!({
"type": "object",
"properties": {
"list": {
"type": "array",
"items": {
"type": "object",
"properties": {
"field": {"type": "string", "description": "Field desc. More detail.", "examples": ["e"]}
},
"required": []
}
}
},
"required": []
});
let (_, p) = minify("desc. more.", ¶ms, SchemaTier::Minimal);
let field = &p["properties"]["list"]["items"]["properties"]["field"];
assert!(field.get("examples").is_none());
assert!(
field.get("description").is_none(),
"not required at that nesting level"
);
assert_eq!(field["type"], "string");
}
#[test]
fn truncate_sentences_ignores_common_abbreviations() {
assert_eq!(
truncate_sentences("See e.g. the docs. Second sentence.", 1),
"See e.g. the docs."
);
assert_eq!(
truncate_sentences(
"Ask Dr. Smith for the etc. items, i.e. all of them. Next.",
1
),
"Ask Dr. Smith for the etc. items, i.e. all of them."
);
assert_eq!(
truncate_sentences("Contact Mr. Lee. Thanks.", 2),
"Contact Mr. Lee. Thanks."
);
assert_eq!(
truncate_sentences("Contact Mr. Lee. Thanks.", 1),
"Contact Mr. Lee."
);
}
#[test]
fn combinators_are_recursed_into_and_minified() {
let params = json!({
"type": "object",
"properties": {
"payload": {
"anyOf": [
{
"type": "object",
"description": "First shape of the payload, used for the legacy request format. It carries a lot of historical baggage. Keep reading for details.",
"properties": {
"a": {"type": "string", "description": "The a field. It represents something important. More context follows here."}
},
"required": ["a"]
},
{
"type": "object",
"description": "Second shape of the payload, used for the modern request format. It is much simpler than the legacy one. Keep reading for details.",
"properties": {
"b": {"type": "integer", "description": "The b field. It represents something else important. More context follows here."}
},
"required": ["b"]
}
]
},
"mode": {
"oneOf": [
{"type": "string", "const": "fast", "description": "Fast mode trades accuracy for speed. Use when latency matters most. Read the docs for tradeoffs."},
{"type": "string", "const": "slow", "description": "Slow mode trades speed for accuracy. Use when correctness matters most. Read the docs for tradeoffs."}
]
},
"combo": {
"allOf": [
{
"type": "object",
"description": "Base combo shape shared by every variant. It defines the common envelope fields. Read carefully before extending.",
"properties": {
"id": {"type": "string", "description": "The identifier. Must be globally unique. Formatted as a UUID."}
},
"required": ["id"]
},
{
"type": "object",
"description": "Extension combo shape layered on top of the base envelope. It adds variant-specific fields. Read carefully before extending.",
"properties": {
"extra": {"type": "string", "description": "Extra data. Optional free-form text. Formatted as plain UTF-8."}
},
"required": []
}
]
}
},
"$defs": {
"Widget": {
"type": "object",
"description": "A reusable widget definition referenced elsewhere in this schema via $ref. It has a long explanatory blurb here for testing.",
"properties": {
"name": {"type": "string", "description": "The widget's name. Must be unique within its namespace. Free-form text otherwise."}
},
"required": ["name"]
}
},
"required": ["payload"]
});
let orig_bytes = serde_json::to_string(¶ms).unwrap().len();
let (_, p) = minify("desc. more. even more.", ¶ms, SchemaTier::Minimal);
let new_bytes = serde_json::to_string(&p).unwrap().len();
assert!(
new_bytes < orig_bytes * 7 / 10,
"expected a meaningful size cut, got {orig_bytes} -> {new_bytes} bytes"
);
let any_of = &p["properties"]["payload"]["anyOf"];
assert_eq!(
any_of[0]["description"],
"First shape of the payload, used for the legacy request format."
);
assert_eq!(any_of[0]["properties"]["a"]["description"], "The a field.");
assert_eq!(any_of[0]["properties"]["a"]["type"], "string");
assert_eq!(any_of[0]["required"], json!(["a"]));
assert_eq!(any_of[1]["properties"]["b"]["type"], "integer");
assert_eq!(any_of[1]["required"], json!(["b"]));
let one_of = &p["properties"]["mode"]["oneOf"];
assert_eq!(
one_of[0]["description"],
"Fast mode trades accuracy for speed."
);
assert_eq!(one_of[0]["type"], "string");
assert_eq!(one_of[0]["const"], "fast");
let all_of = &p["properties"]["combo"]["allOf"];
assert_eq!(
all_of[0]["description"],
"Base combo shape shared by every variant."
);
assert_eq!(
all_of[0]["properties"]["id"]["description"],
"The identifier."
);
assert!(all_of[1]["properties"]["extra"]
.get("description")
.is_none());
assert_eq!(all_of[1]["required"], json!([]));
let widget = &p["$defs"]["Widget"];
assert_eq!(
widget["description"],
"A reusable widget definition referenced elsewhere in this schema via $ref."
);
assert_eq!(
widget["properties"]["name"]["description"],
"The widget's name."
);
assert_eq!(widget["properties"]["name"]["type"], "string");
assert_eq!(widget["required"], json!(["name"]));
assert_eq!(p["required"], json!(["payload"]));
assert_eq!(p["type"], "object");
}
#[test]
fn if_then_else_definitions_and_pattern_properties_are_recursed_into() {
let params = json!({
"type": "object",
"if": {"type": "object", "description": "Condition branch description. More detail here.", "properties": {"x": {"type": "string"}}},
"then": {"type": "object", "description": "Then branch description. More detail here.", "properties": {"y": {"type": "string", "description": "Y field. More detail here."}}, "required": ["y"]},
"else": {"type": "object", "description": "Else branch description. More detail here.", "properties": {"z": {"type": "string", "description": "Z field. More detail here."}}, "required": []},
"definitions": {
"Old": {"type": "object", "description": "Legacy definition kept for draft-7 compatibility. More detail here.", "properties": {"n": {"type": "string"}}}
},
"patternProperties": {
"^S_": {"type": "string", "description": "Pattern-matched string property. More detail here."}
},
"properties": {},
"required": []
});
let (_, p) = minify("desc. more.", ¶ms, SchemaTier::Minimal);
assert_eq!(p["if"]["description"], "Condition branch description.");
assert_eq!(p["then"]["description"], "Then branch description.");
assert_eq!(p["then"]["properties"]["y"]["description"], "Y field.");
assert_eq!(p["else"]["description"], "Else branch description.");
assert!(p["else"]["properties"]["z"].get("description").is_none());
assert_eq!(
p["definitions"]["Old"]["description"],
"Legacy definition kept for draft-7 compatibility."
);
assert_eq!(
p["patternProperties"]["^S_"]["description"],
"Pattern-matched string property."
);
}
#[test]
fn tuple_style_items_array_is_recursed_into() {
let params = json!({
"type": "array",
"items": [
{"type": "string", "description": "First tuple slot description. More detail here.", "examples": ["e"]},
{"type": "integer", "description": "Second tuple slot description. More detail here.", "title": "t"}
]
});
let (_, p) = minify("desc. more.", ¶ms, SchemaTier::Minimal);
assert_eq!(
p["items"][0]["description"],
"First tuple slot description."
);
assert!(p["items"][0].get("examples").is_none());
assert_eq!(p["items"][0]["type"], "string");
assert_eq!(
p["items"][1]["description"],
"Second tuple slot description."
);
assert!(p["items"][1].get("title").is_none());
assert_eq!(p["items"][1]["type"], "integer");
}
}