use std::collections::{BTreeSet, HashMap, HashSet};
use serde_json::{Map, Value};
use super::document::{Document, JsonPointer, RefError, SpecVersion};
use crate::tools::tool_name::sanitize_segment;
const MAX_ALLOF_REFS: usize = 64;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum SchemaDirection {
Request,
#[cfg_attr(
not(test),
expect(
dead_code,
reason = "the signature renderer will render response schemas"
)
)]
Response,
}
impl SchemaDirection {
fn hidden_keyword(self) -> &'static str {
match self {
Self::Request => "readOnly",
Self::Response => "writeOnly",
}
}
}
const SUBSCHEMA_KEYWORDS: &[&str] = &[
"items",
"additionalItems",
"additionalProperties",
"unevaluatedItems",
"unevaluatedProperties",
"not",
"if",
"then",
"else",
"contains",
"propertyNames",
"allOf",
"anyOf",
"oneOf",
"prefixItems",
];
const SUBSCHEMA_MAP_KEYWORDS: &[&str] = &[
"properties",
"patternProperties",
"dependentSchemas",
"$defs",
"definitions",
];
fn for_each_subschema<E>(
schema: &Map<String, Value>,
mut visit: impl FnMut(&Value) -> Result<(), E>,
) -> Result<(), E> {
for (keyword, value) in schema {
let keyword = keyword.as_str();
match value {
Value::Array(items) if SUBSCHEMA_KEYWORDS.contains(&keyword) => {
items.iter().try_for_each(&mut visit)?;
}
Value::Object(entries) if SUBSCHEMA_MAP_KEYWORDS.contains(&keyword) => {
entries.values().try_for_each(&mut visit)?;
}
subschema if SUBSCHEMA_KEYWORDS.contains(&keyword) => visit(subschema)?,
_ => {}
}
}
Ok(())
}
fn for_each_subschema_mut<E>(
schema: &mut Map<String, Value>,
mut visit: impl FnMut(&str, &mut Value) -> Result<(), E>,
) -> Result<(), E> {
for (keyword, value) in schema.iter_mut() {
let keyword = keyword.as_str();
match value {
Value::Array(items) if SUBSCHEMA_KEYWORDS.contains(&keyword) => {
for item in items {
visit(keyword, item)?;
}
}
Value::Object(entries) if SUBSCHEMA_MAP_KEYWORDS.contains(&keyword) => {
for entry in entries.values_mut() {
visit(keyword, entry)?;
}
}
subschema if SUBSCHEMA_KEYWORDS.contains(&keyword) => visit(keyword, subschema)?,
_ => {}
}
}
Ok(())
}
pub(super) fn check_refs(
document: &Document,
roots: Vec<JsonPointer>,
verified: &mut HashSet<JsonPointer>,
) -> Result<(), RefError> {
let mut pending = roots;
let mut visited = HashSet::new();
while let Some(at) = pending.pop() {
if verified.contains(&at) || !visited.insert(at.clone()) {
continue;
}
let schema = document
.get(&at)
.ok_or_else(|| RefError::Broken(at.to_string()))?;
collect_refs(document, schema, &mut pending)?;
}
verified.extend(visited);
Ok(())
}
fn collect_refs(
document: &Document,
schema: &Value,
pending: &mut Vec<JsonPointer>,
) -> Result<(), RefError> {
let Value::Object(map) = schema else {
return Ok(());
};
if let Some(target) = document.schema_ref_target(map)? {
pending.push(target);
if document.version().ignores_ref_siblings() {
return Ok(());
}
}
for_each_subschema(map, |subschema| collect_refs(document, subschema, pending))
}
#[derive(Debug)]
pub(crate) struct RenderedSchemas {
pub(crate) roots: Vec<Value>,
pub(crate) defs: Map<String, Value>,
}
impl Document {
#[cfg(test)]
pub(crate) fn render_schema(
&self,
at: &JsonPointer,
direction: SchemaDirection,
) -> Result<Value, RefError> {
let RenderedSchemas { mut roots, defs } = self.render_schemas([at], direction)?;
let mut rendered = roots.pop().unwrap_or_default();
if !defs.is_empty() {
if let Value::Object(map) = &mut rendered {
map.insert("$defs".to_owned(), Value::Object(defs));
}
}
Ok(rendered)
}
pub(crate) fn render_schemas<'a>(
&self,
ats: impl IntoIterator<Item = &'a JsonPointer>,
direction: SchemaDirection,
) -> Result<RenderedSchemas, RefError> {
let mut renderer = Renderer {
document: self,
direction,
def_keys: HashMap::new(),
used_keys: HashSet::new(),
pending: Vec::new(),
};
let mut roots = Vec::new();
for at in ats {
let mut rendered = self
.get(at)
.ok_or_else(|| RefError::Broken(at.to_string()))?
.clone();
renderer.transform(&mut rendered, Hiding::On(&BTreeSet::new()))?;
roots.push(rendered);
}
let mut defs = Map::new();
while let Some((target, mode, key)) = renderer.pending.pop() {
let mut def = self
.get(&target)
.cloned()
.ok_or_else(|| RefError::Broken(target.to_string()))?;
let hiding = match mode {
DefMode::Hiding => Hiding::On(&BTreeSet::new()),
DefMode::Verbatim => Hiding::Off,
};
renderer.transform(&mut def, hiding)?;
defs.insert(key, def);
}
defs.sort_keys();
Ok(RenderedSchemas { roots, defs })
}
}
struct Renderer<'a> {
document: &'a Document,
direction: SchemaDirection,
def_keys: HashMap<(JsonPointer, DefMode), String>,
used_keys: HashSet<String>,
pending: Vec<(JsonPointer, DefMode, String)>,
}
impl Renderer<'_> {
fn transform(&mut self, schema: &mut Value, hiding: Hiding<'_>) -> Result<(), RefError> {
let Value::Object(map) = schema else {
return Ok(());
};
let target = self.document.schema_ref_target(map)?;
let mode = match hiding {
Hiding::On(_) => DefMode::Hiding,
Hiding::Off => DefMode::Verbatim,
};
if let (Some(target), true) = (&target, self.document.version().ignores_ref_siblings()) {
let reference = self.def_ref(target.clone(), mode);
*map = Map::from_iter([("$ref".to_owned(), reference)]);
return Ok(());
}
let hidden = match hiding {
Hiding::Off => None,
Hiding::On(inherited) => {
let mut hidden = self.hidden_names(map)?;
hidden.extend(inherited.iter().cloned());
Some(hidden)
}
};
if let Some(target) = target {
let reference = self.def_ref(target, mode);
map.insert("$ref".to_owned(), reference);
}
match self.document.version() {
SpecVersion::V3_0 => normalize_3_0(map),
SpecVersion::V3_1 => {}
}
if let Some(hidden) = hidden.as_ref().filter(|hidden| !hidden.is_empty()) {
if let Some(Value::Object(properties)) = map.get_mut("properties") {
properties.retain(|name, _| !hidden.contains(name));
}
if let Some(Value::Array(required)) = map.get_mut("required") {
required.retain(|name| !name.as_str().is_some_and(|name| hidden.contains(name)));
}
}
let empty = BTreeSet::new();
for_each_subschema_mut(map, |keyword, subschema| {
let child = match (hidden.as_ref(), keyword) {
(None, _) | (_, "not" | "if" | "contains") => Hiding::Off,
(Some(hidden), "allOf") => Hiding::On(hidden),
(Some(_), _) => Hiding::On(&empty),
};
self.transform(subschema, child)
})
}
fn hidden_names(&self, schema: &Map<String, Value>) -> Result<BTreeSet<String>, RefError> {
let mut hidden = BTreeSet::new();
let mut visited = HashSet::new();
let mut pending = vec![schema];
while let Some(schema) = pending.pop() {
if let Some(target) = self.document.schema_ref_target(schema)? {
if visit(&mut visited, &target)? {
if let Some(Value::Object(target)) = self.document.get(&target) {
pending.push(target);
}
}
if self.document.version().ignores_ref_siblings() {
continue;
}
}
if let Some(Value::Object(properties)) = schema.get("properties") {
for (name, property) in properties {
if self.marks_hidden(property)? {
hidden.insert(name.clone());
}
}
}
pending.extend(branches(schema, "allOf").filter_map(Value::as_object));
}
Ok(hidden)
}
fn marks_hidden(&self, schema: &Value) -> Result<bool, RefError> {
let keyword = self.direction.hidden_keyword();
let mut visited = HashSet::new();
let mut pending = vec![schema];
while let Some(schema) = pending.pop() {
let Value::Object(map) = schema else {
continue;
};
if map.get(keyword) == Some(&Value::Bool(true)) {
return Ok(true);
}
if let Some(target) = self.document.schema_ref_target(map)? {
if visit(&mut visited, &target)? {
if let Some(target) = self.document.get(&target) {
pending.push(target);
}
}
if self.document.version().ignores_ref_siblings() {
continue;
}
}
pending.extend(branches(map, "allOf"));
}
Ok(false)
}
fn def_ref(&mut self, target: JsonPointer, mode: DefMode) -> Value {
Value::String(format!("#/$defs/{}", self.def_key(target, mode)))
}
fn def_key(&mut self, target: JsonPointer, mode: DefMode) -> String {
if let Some(key) = self.def_keys.get(&(target.clone(), mode)) {
return key.clone();
}
let last = target.segments().last().unwrap_or_default();
let readable = !last.is_empty()
&& last
.chars()
.all(|character| character.is_ascii_alphanumeric() || "._-".contains(character));
let base = if readable {
last
} else {
sanitize_segment(&last)
};
let mut key = base.clone();
let mut suffix = 2;
while !self.used_keys.insert(key.clone()) {
key = format!("{base}_{suffix}");
suffix += 1;
}
self.def_keys.insert((target.clone(), mode), key.clone());
self.pending.push((target, mode, key.clone()));
key
}
}
#[derive(Clone, Copy)]
enum Hiding<'a> {
Off,
On(&'a BTreeSet<String>),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum DefMode {
Hiding,
Verbatim,
}
fn visit(visited: &mut HashSet<JsonPointer>, target: &JsonPointer) -> Result<bool, RefError> {
if visited.contains(target) {
return Ok(false);
}
if visited.len() == MAX_ALLOF_REFS {
return Err(RefError::TooManyAllOf {
reference: target.to_string(),
limit: MAX_ALLOF_REFS,
});
}
Ok(visited.insert(target.clone()))
}
fn branches<'a>(schema: &'a Map<String, Value>, keyword: &str) -> impl Iterator<Item = &'a Value> {
schema
.get(keyword)
.and_then(Value::as_array)
.into_iter()
.flatten()
}
fn normalize_3_0(schema: &mut Map<String, Value>) {
for (flag, bound) in [
("exclusiveMinimum", "minimum"),
("exclusiveMaximum", "maximum"),
] {
if let Some(Value::Bool(exclusive)) = schema.get(flag) {
let exclusive = *exclusive;
schema.remove(flag);
match schema.remove(bound) {
Some(limit) if exclusive => schema.insert(flag.to_owned(), limit),
Some(limit) => schema.insert(bound.to_owned(), limit),
None => None,
};
}
}
if schema.remove("nullable") != Some(Value::Bool(true)) {
return;
}
if let Some(kind) = schema.get_mut("type") {
let null = Value::String("null".to_owned());
match kind {
Value::Array(kinds) if !kinds.contains(&null) => kinds.push(null),
Value::String(_) => *kind = Value::Array(vec![kind.take(), null]),
_ => {}
}
if let Some(Value::Array(values)) = schema.get_mut("enum") {
if !values.contains(&Value::Null) {
values.push(Value::Null);
}
}
return;
}
if ["allOf", "anyOf", "oneOf"]
.iter()
.any(|keyword| schema.contains_key(*keyword))
{
let inner = std::mem::take(schema);
let null = Value::Object(Map::from_iter([(
"type".to_owned(),
Value::String("null".to_owned()),
)]));
schema.insert(
"anyOf".to_owned(),
Value::Array(vec![Value::Object(inner), null]),
);
}
}