rust_web_server/schema/
mod.rs1use std::cell::Cell;
15use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque};
16use std::rc::Rc;
17use std::sync::Arc;
18
19use crate::mcp::json_escape;
20
21pub trait JsonSchema {
23 fn json_schema() -> String;
25
26 fn is_optional() -> bool {
28 false
29 }
30}
31
32pub const MAX_DEPTH: u32 = 32;
36
37thread_local! {
38 static DEPTH: Cell<u32> = const { Cell::new(0) };
39}
40
41pub fn nested<T: JsonSchema + ?Sized>() -> String {
44 let depth = DEPTH.with(|d| d.get());
45 if depth >= MAX_DEPTH {
46 return "{}".to_string();
47 }
48 DEPTH.with(|d| d.set(depth + 1));
49 let schema = T::json_schema();
50 DEPTH.with(|d| d.set(depth));
51 schema
52}
53
54pub fn with_description(schema: &str, description: &str) -> String {
56 if description.is_empty() {
57 return schema.to_string();
58 }
59 let rest = schema.trim_start().strip_prefix('{').unwrap_or("}").trim_start();
60 let separator = if rest.starts_with('}') { "" } else { "," };
61 format!(r#"{{"description":"{}"{separator}{rest}"#, json_escape(description))
62}
63
64pub struct ObjectSchema {
67 description: String,
68 properties: Vec<String>,
69 required: Vec<String>,
70}
71
72impl ObjectSchema {
73 pub fn new(description: &str) -> Self {
75 ObjectSchema { description: description.to_string(), properties: Vec::new(), required: Vec::new() }
76 }
77
78 pub fn property(&mut self, name: &str, schema: &str, description: &str, required: bool) {
80 self.properties.push(format!(r#""{}":{}"#, json_escape(name), with_description(schema, description)));
81 if required {
82 self.required.push(format!(r#""{}""#, json_escape(name)));
83 }
84 }
85
86 pub fn finish(self) -> String {
88 let required = if self.required.is_empty() {
89 String::new()
90 } else {
91 format!(r#","required":[{}]"#, self.required.join(","))
92 };
93 let schema = format!(r#"{{"type":"object","properties":{{{}}}{required}}}"#, self.properties.join(","));
94 with_description(&schema, &self.description)
95 }
96}
97
98pub fn string_enum(description: &str, values: &[&str]) -> String {
100 let values: Vec<String> = values.iter().map(|v| format!(r#""{}""#, json_escape(v))).collect();
101 with_description(&format!(r#"{{"type":"string","enum":[{}]}}"#, values.join(",")), description)
102}
103
104pub fn is_object_schema(schema: &str) -> bool {
107 crate::mcp::json_rpc::object_members(schema)
108 .and_then(|members| members.into_iter().find(|(k, _)| k == "type"))
109 .is_some_and(|(_, v)| v == r#""object""#)
110}
111
112macro_rules! fixed_schema {
113 ($schema:expr => $($t:ty),*) => {
114 $(impl JsonSchema for $t {
115 fn json_schema() -> String { $schema.to_string() }
116 })*
117 };
118}
119
120fixed_schema!(r#"{"type":"boolean"}"# => bool);
121fixed_schema!(r#"{"type":"integer"}"# => i8, i16, i32, i64, i128, isize);
122fixed_schema!(r#"{"type":"integer","minimum":0}"# => u8, u16, u32, u64, u128, usize);
123fixed_schema!(r#"{"type":"number"}"# => f32, f64);
124fixed_schema!(r#"{"type":"string"}"# => String, str, char);
125fixed_schema!(r#"{"type":"null"}"# => ());
126
127impl<T: JsonSchema> JsonSchema for Option<T> {
128 fn json_schema() -> String {
129 format!(r#"{{"anyOf":[{},{{"type":"null"}}]}}"#, nested::<T>())
130 }
131 fn is_optional() -> bool {
132 true
133 }
134}
135
136macro_rules! array_schema {
137 ($($t:ident),*) => {
138 $(impl<T: JsonSchema> JsonSchema for $t<T> {
139 fn json_schema() -> String { format!(r#"{{"type":"array","items":{}}}"#, nested::<T>()) }
140 })*
141 };
142}
143
144array_schema!(Vec, VecDeque, BTreeSet);
145
146impl<T: JsonSchema, S> JsonSchema for HashSet<T, S> {
147 fn json_schema() -> String {
148 format!(r#"{{"type":"array","items":{}}}"#, nested::<T>())
149 }
150}
151
152impl<T: JsonSchema> JsonSchema for [T] {
153 fn json_schema() -> String {
154 format!(r#"{{"type":"array","items":{}}}"#, nested::<T>())
155 }
156}
157
158impl<T: JsonSchema, const N: usize> JsonSchema for [T; N] {
159 fn json_schema() -> String {
160 format!(r#"{{"type":"array","items":{},"minItems":{N},"maxItems":{N}}}"#, nested::<T>())
161 }
162}
163
164impl<T: JsonSchema, S> JsonSchema for HashMap<String, T, S> {
165 fn json_schema() -> String {
166 format!(r#"{{"type":"object","additionalProperties":{}}}"#, nested::<T>())
167 }
168}
169
170impl<T: JsonSchema> JsonSchema for BTreeMap<String, T> {
171 fn json_schema() -> String {
172 format!(r#"{{"type":"object","additionalProperties":{}}}"#, nested::<T>())
173 }
174}
175
176macro_rules! pointer_schema {
177 ($($t:ident),*) => {
178 $(impl<T: JsonSchema + ?Sized> JsonSchema for $t<T> {
179 fn json_schema() -> String { T::json_schema() }
180 fn is_optional() -> bool { T::is_optional() }
181 })*
182 };
183}
184
185pointer_schema!(Box, Arc, Rc);
186
187impl<T: JsonSchema + ?Sized> JsonSchema for &T {
188 fn json_schema() -> String {
189 T::json_schema()
190 }
191 fn is_optional() -> bool {
192 T::is_optional()
193 }
194}
195
196#[cfg(feature = "serde")]
198impl JsonSchema for serde_json::Value {
199 fn json_schema() -> String {
200 "{}".to_string()
201 }
202}
203
204#[cfg(test)]
205mod tests;