1use std::collections::BTreeMap;
9
10#[derive(Debug, Clone)]
13pub struct Schema {
14 inner: SchemaInner,
15}
16
17#[derive(Debug, Clone)]
18enum SchemaInner {
19 Built(Node),
20 Raw(Vec<u8>),
22}
23
24#[derive(Debug, Clone)]
25struct Node {
26 kind: Kind,
27 description: Option<String>,
28 properties: BTreeMap<String, Node>,
29 required: Vec<String>,
30 additional_properties: Option<bool>,
31 enum_values: Vec<String>,
32 items: Option<Box<Node>>,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36enum Kind {
37 Object,
38 String,
39 Number,
40 Integer,
41 Boolean,
42 Array,
43}
44
45impl Default for Node {
46 fn default() -> Self {
47 Self {
48 kind: Kind::Object,
49 description: None,
50 properties: BTreeMap::new(),
51 required: Vec::new(),
52 additional_properties: None,
53 enum_values: Vec::new(),
54 items: None,
55 }
56 }
57}
58
59impl serde::Serialize for Node {
63 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
64 use serde::ser::SerializeMap;
65 let mut m = s.serialize_map(Some(3))?;
66 m.serialize_entry("type", kind_str(self.kind))?;
67 if let Some(d) = &self.description {
68 m.serialize_entry("description", d)?;
69 }
70 if self.kind == Kind::Object {
71 m.serialize_entry("properties", &self.properties)?;
72 if !self.required.is_empty() {
73 m.serialize_entry("required", &self.required)?;
74 }
75 if let Some(allow) = self.additional_properties {
76 m.serialize_entry("additionalProperties", &allow)?;
77 }
78 }
79 if self.kind == Kind::String && !self.enum_values.is_empty() {
80 m.serialize_entry("enum", &self.enum_values)?;
81 }
82 if self.kind == Kind::Array {
83 if let Some(items) = &self.items {
84 m.serialize_entry("items", items.as_ref())?;
85 }
86 }
87 m.end()
88 }
89}
90
91impl Schema {
92 fn from_node(node: Node) -> Self {
93 Self {
94 inner: SchemaInner::Built(node),
95 }
96 }
97
98 pub fn object() -> Self {
100 Self::from_node(Node {
101 kind: Kind::Object,
102 ..Node::default()
103 })
104 }
105
106 pub fn string() -> Self {
107 Self::from_node(Node {
108 kind: Kind::String,
109 ..Node::default()
110 })
111 }
112
113 pub fn number() -> Self {
114 Self::from_node(Node {
115 kind: Kind::Number,
116 ..Node::default()
117 })
118 }
119
120 pub fn integer() -> Self {
121 Self::from_node(Node {
122 kind: Kind::Integer,
123 ..Node::default()
124 })
125 }
126
127 pub fn boolean() -> Self {
128 Self::from_node(Node {
129 kind: Kind::Boolean,
130 ..Node::default()
131 })
132 }
133
134 pub fn array(items: Schema) -> Self {
136 let SchemaInner::Built(items_node) = items.inner else {
137 panic!("Schema::array requires a builder schema, not Schema::raw");
138 };
139 Self::from_node(Node {
140 kind: Kind::Array,
141 items: Some(Box::new(items_node)),
142 ..Node::default()
143 })
144 }
145
146 pub fn raw(json: impl Into<Vec<u8>>) -> Self {
148 Self {
149 inner: SchemaInner::Raw(json.into()),
150 }
151 }
152
153 pub fn description(mut self, d: impl Into<String>) -> Self {
154 if let SchemaInner::Built(n) = &mut self.inner {
155 n.description = Some(d.into());
156 }
157 self
158 }
159
160 pub fn property(mut self, name: impl Into<String>, schema: Schema) -> Self {
162 if let SchemaInner::Built(n) = &mut self.inner {
163 if n.kind == Kind::Object {
164 if let SchemaInner::Built(child) = schema.inner {
165 n.properties.insert(name.into(), child);
166 }
167 }
168 }
169 self
170 }
171
172 pub fn required<I, S>(mut self, names: I) -> Self
174 where
175 I: IntoIterator<Item = S>,
176 S: Into<String>,
177 {
178 if let SchemaInner::Built(n) = &mut self.inner {
179 if n.kind == Kind::Object {
180 n.required.extend(names.into_iter().map(Into::into));
181 }
182 }
183 self
184 }
185
186 pub fn additional_properties(mut self, allow: bool) -> Self {
187 if let SchemaInner::Built(n) = &mut self.inner {
188 if n.kind == Kind::Object {
189 n.additional_properties = Some(allow);
190 }
191 }
192 self
193 }
194
195 pub fn enum_values<I, S>(mut self, values: I) -> Self
197 where
198 I: IntoIterator<Item = S>,
199 S: Into<String>,
200 {
201 if let SchemaInner::Built(n) = &mut self.inner {
202 if n.kind == Kind::String {
203 n.enum_values.extend(values.into_iter().map(Into::into));
204 }
205 }
206 self
207 }
208
209 pub fn to_json_bytes(&self) -> Vec<u8> {
211 match &self.inner {
212 SchemaInner::Raw(b) => b.clone(),
213 SchemaInner::Built(n) => {
214 serde_json::to_vec(n).expect("schema serialization cannot fail")
215 }
216 }
217 }
218}
219
220impl From<Vec<u8>> for Schema {
221 fn from(json: Vec<u8>) -> Self {
222 Schema::raw(json)
223 }
224}
225
226impl From<&[u8]> for Schema {
227 fn from(json: &[u8]) -> Self {
228 Schema::raw(json.to_vec())
229 }
230}
231
232fn kind_str(k: Kind) -> &'static str {
233 match k {
234 Kind::Object => "object",
235 Kind::String => "string",
236 Kind::Number => "number",
237 Kind::Integer => "integer",
238 Kind::Boolean => "boolean",
239 Kind::Array => "array",
240 }
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246
247 #[test]
248 fn object_with_required_string_prop() {
249 let s = Schema::object()
250 .property("text", Schema::string().description("input text"))
251 .required(["text"]);
252 assert_eq!(
253 String::from_utf8(s.to_json_bytes()).unwrap(),
254 r#"{"type":"object","properties":{"text":{"type":"string","description":"input text"}},"required":["text"]}"#
255 );
256 }
257
258 #[test]
259 fn string_enum_and_additional_properties() {
260 let s = Schema::object()
261 .property(
262 "mode",
263 Schema::string().enum_values(["read-only", "workspace-write"]),
264 )
265 .additional_properties(false);
266 let json = String::from_utf8(s.to_json_bytes()).unwrap();
267 assert!(json.contains(r#""enum":["read-only","workspace-write"]"#));
268 assert!(json.contains(r#""additionalProperties":false"#));
269 }
270
271 #[test]
272 fn array_of_strings() {
273 let s = Schema::object().property("tags", Schema::array(Schema::string()));
274 assert_eq!(
275 String::from_utf8(s.to_json_bytes()).unwrap(),
276 r#"{"type":"object","properties":{"tags":{"type":"array","items":{"type":"string"}}}}"#
277 );
278 }
279
280 #[test]
281 fn raw_passthrough() {
282 let raw = br#"{"type":"object"}"#;
283 assert_eq!(Schema::raw(raw.to_vec()).to_json_bytes(), raw);
284 }
285}