use std::collections::HashSet;
use anyhow::{Context, Result, bail};
use schemars::schema::{
ArrayValidation, InstanceType, ObjectValidation, Schema, SchemaObject, SingleOrVec,
};
use serde_json::Value;
pub(super) fn required_set(object: &ObjectValidation) -> HashSet<String> {
object.required.iter().cloned().collect()
}
pub(super) fn to_pointer(path: &[String]) -> String {
if path.is_empty() {
return String::new();
}
path.iter()
.map(|segment| segment.replace('~', "~0").replace('/', "~1"))
.fold(String::new(), |mut acc, segment| {
acc.push('/');
acc.push_str(&segment);
acc
})
}
pub(super) fn instance_type(schema: &SchemaObject) -> Option<InstanceType> {
schema.instance_type.as_ref().and_then(|kind| match kind {
SingleOrVec::Single(single) => Some(**single),
SingleOrVec::Vec(items) => items
.iter()
.cloned()
.find(|item| *item != InstanceType::Null),
})
}
pub(super) fn is_object_schema(schema: &SchemaObject) -> bool {
match instance_type(schema) {
Some(InstanceType::Object) => true,
None => schema.object.is_some(),
_ => false,
}
}
pub(super) fn ensure_object_schema(schema: &SchemaObject) -> Result<()> {
if is_object_schema(schema) {
Ok(())
} else {
bail!("schema must describe an object")
}
}
pub(super) fn has_composite_subschemas(schema: &SchemaObject) -> bool {
schema
.subschemas
.as_ref()
.map(|subs| subs.one_of.is_some() || subs.any_of.is_some())
.unwrap_or(false)
}
pub(super) fn schema_object_to_value(schema: &SchemaObject) -> Result<Value> {
serde_json::to_value(Schema::Object(schema.clone()))
.context("failed to serialize schema object")
}
pub(super) fn schema_titles(
schema: &SchemaObject,
fallback: &str,
) -> (String, Option<String>, Option<Value>) {
let title = schema
.metadata
.as_ref()
.and_then(|m| m.title.clone())
.unwrap_or_else(|| fallback.to_string());
let description = schema.metadata.as_ref().and_then(|m| m.description.clone());
let default = schema.metadata.as_ref().and_then(|m| m.default.clone());
(title, description, default)
}
pub(super) fn key_value_entry_schema(key_schema: &Value, value_schema: &Value) -> Value {
serde_json::json!({
"type": "object",
"required": ["key", "value"],
"properties": {
"key": key_schema,
"value": value_schema,
}
})
}
pub(super) fn describe_schema_shape(schema: &SchemaObject) -> String {
if let Some(instance) = instance_type(schema) {
match instance {
InstanceType::String => {
if let Some(format) = schema.format.as_ref() {
return format!("string({})", format);
}
return "string".to_string();
}
InstanceType::Integer => {
let mut parts = vec!["integer".to_string()];
if let Some(number) = schema.number.as_ref()
&& (number.minimum.is_some() || number.maximum.is_some())
{
let min = number
.minimum
.as_ref()
.map(|n| format!("{:.0}", n))
.unwrap_or("*".to_string());
let max = number
.maximum
.as_ref()
.map(|n| format!("{:.0}", n))
.unwrap_or("*".to_string());
parts.push(format!("[{}..{}]", min, max));
}
return parts.join(" ");
}
InstanceType::Number => return "number".to_string(),
InstanceType::Boolean => return "boolean".to_string(),
InstanceType::Array => {
if let Some(array) = schema.array.as_ref()
&& let Some(item) = describe_array_items(array)
{
return if item.is_empty() || item == "any" {
"array".to_string()
} else {
format!("{}[]", item)
};
}
return "array".to_string();
}
InstanceType::Object => return describe_object_shape(schema),
InstanceType::Null => {}
}
}
if let Some(subschemas) = schema.subschemas.as_ref() {
if let Some(one_of) = subschemas.one_of.as_ref()
&& !one_of.is_empty()
{
return "oneOf".to_string();
}
if let Some(any_of) = subschemas.any_of.as_ref()
&& !any_of.is_empty()
{
return "anyOf".to_string();
}
}
if schema
.enum_values
.as_ref()
.is_some_and(|values| !values.is_empty())
{
return "enum".to_string();
}
String::new()
}
pub(super) fn describe_object_shape(schema: &SchemaObject) -> String {
if let Some(object) = schema.object.as_ref()
&& !object.properties.is_empty()
{
let mut props: Vec<String> = object.properties.keys().take(3).cloned().collect();
if object.properties.len() > 3 {
props.push("…".to_string());
}
return format!("object({})", props.join(", "));
}
"object".to_string()
}
pub(super) fn describe_array_items(array: &ArrayValidation) -> Option<String> {
let items = array.items.as_ref()?;
match items {
SingleOrVec::Single(schema) => Some(describe_schema_from_single(schema.as_ref())),
SingleOrVec::Vec(list) => list.first().map(describe_schema_from_single),
}
}
pub(super) fn describe_schema_from_single(schema: &Schema) -> String {
match schema {
Schema::Bool(true) => "any".to_string(),
Schema::Bool(false) => "never".to_string(),
Schema::Object(obj) => describe_schema_shape(obj),
}
}