pub use cloacina_api_types::InputSlot;
pub fn schema_for<T: schemars::JsonSchema>() -> serde_json::Value {
let root = schemars::gen::SchemaGenerator::default().into_root_schema_for::<T>();
serde_json::to_value(root).unwrap_or(serde_json::Value::Null)
}
pub fn default_json<T: serde::Serialize>(value: T) -> Option<serde_json::Value> {
serde_json::to_value(value).ok()
}
pub struct SchemaProbe<T>(core::marker::PhantomData<T>);
impl<T> SchemaProbe<T> {
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
SchemaProbe(core::marker::PhantomData)
}
}
pub trait ProbeTyped {
fn probe_input_schema(&self) -> serde_json::Value;
}
impl<T: schemars::JsonSchema> ProbeTyped for SchemaProbe<T> {
fn probe_input_schema(&self) -> serde_json::Value {
schema_for::<T>()
}
}
pub trait ProbeFallback {
fn probe_input_schema(&self) -> serde_json::Value;
}
impl<T> ProbeFallback for &SchemaProbe<T> {
fn probe_input_schema(&self) -> serde_json::Value {
serde_json::json!({})
}
}
pub fn slots_to_json(slots: &[InputSlot]) -> String {
serde_json::to_string(slots).unwrap_or_else(|_| "[]".to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(JsonSchema, Serialize, Deserialize)]
#[allow(dead_code)]
struct SampleBoundary {
order_id: String,
limit: u32,
enabled: bool,
}
#[test]
fn schema_for_scalar_type() {
let schema = schema_for::<String>();
assert_eq!(
schema.get("type").and_then(|t| t.as_str()),
Some("string"),
"String should produce a string-typed JSON Schema, got: {schema}"
);
}
#[test]
fn schema_for_struct_has_properties() {
let schema = schema_for::<SampleBoundary>();
let props = schema
.get("properties")
.and_then(|p| p.as_object())
.expect("struct schema has properties");
assert!(props.contains_key("order_id"));
assert!(props.contains_key("limit"));
assert!(props.contains_key("enabled"));
}
#[derive(Serialize, Deserialize)]
#[allow(dead_code)]
struct UntypedBoundary {
raw: Vec<u8>,
}
#[test]
fn probe_typed_boundary_yields_real_schema() {
use super::{ProbeFallback as _, ProbeTyped as _};
let schema = (&SchemaProbe::<SampleBoundary>::new()).probe_input_schema();
assert!(
schema.get("properties").is_some(),
"JsonSchema boundary should yield a real object schema, got: {schema}"
);
}
#[test]
fn probe_untyped_boundary_falls_back_to_any() {
use super::{ProbeFallback as _, ProbeTyped as _};
let schema = (&SchemaProbe::<UntypedBoundary>::new()).probe_input_schema();
assert_eq!(
schema,
serde_json::json!({}),
"non-JsonSchema boundary should fall back to a permissive schema, got: {schema}"
);
}
#[test]
fn probe_scalar_typed_yields_schema() {
use super::{ProbeFallback as _, ProbeTyped as _};
let schema = (&SchemaProbe::<String>::new()).probe_input_schema();
assert_eq!(schema.get("type").and_then(|t| t.as_str()), Some("string"));
}
#[test]
fn slots_round_trip_through_json() {
let slots = vec![
InputSlot::required("order_id", schema_for::<String>()),
InputSlot::optional("limit", schema_for::<u32>(), default_json(100u32)),
];
let json = slots_to_json(&slots);
let back: Vec<InputSlot> = serde_json::from_str(&json).expect("parse slots_json");
assert_eq!(back.len(), 2);
assert_eq!(back[0].name, "order_id");
assert!(back[0].required);
assert_eq!(back[1].name, "limit");
assert!(!back[1].required);
assert_eq!(back[1].default, Some(serde_json::json!(100)));
}
}