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()
}