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::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 _;
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::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!(!back[0].encrypted);
assert_eq!(back[1].name, "limit");
assert!(!back[1].required);
assert_eq!(back[1].default, Some(serde_json::json!(100)));
}
#[test]
fn secret_slot_round_trips_with_encrypted_marker() {
let slots = vec![
InputSlot::required("order_id", schema_for::<String>()),
InputSlot::secret("db_prod"),
];
let json = slots_to_json(&slots);
assert!(json.contains("\"encrypted\":true"), "json: {json}");
let back: Vec<InputSlot> = serde_json::from_str(&json).expect("parse slots_json");
let secret = back.iter().find(|s| s.name == "db_prod").unwrap();
assert!(secret.encrypted);
assert!(secret.required);
assert!(secret.default.is_none());
assert!(
!back
.iter()
.find(|s| s.name == "order_id")
.unwrap()
.encrypted
);
}
#[test]
fn legacy_slot_json_without_encrypted_field_defaults_to_plaintext() {
let legacy = r#"[{"name":"x","schema":{"type":"string"},"required":true}]"#;
let back: Vec<InputSlot> = serde_json::from_str(legacy).expect("parse legacy slots");
assert_eq!(back.len(), 1);
assert!(!back[0].encrypted);
}
}