1use serde::{Deserialize, Serialize};
21use std::{borrow::Cow, collections::BTreeMap};
22
23use crate::protocol::version::const_string;
25
26const_string!(ObjectTypeConst = "object");
31const_string!(StringTypeConst = "string");
32const_string!(NumberTypeConst = "number");
33const_string!(IntegerTypeConst = "integer");
34const_string!(BooleanTypeConst = "boolean");
35const_string!(EnumTypeConst = "string");
36const_string!(ArrayTypeConst = "array");
37
38#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
49#[serde(untagged)]
50pub enum PrimitiveSchema {
51 Enum(EnumSchema),
53 String(StringSchema),
55 Number(NumberSchema),
57 Integer(IntegerSchema),
59 Boolean(BooleanSchema),
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
69#[serde(rename_all = "kebab-case")]
70pub enum StringFormat {
71 Email,
73 Uri,
75 Date,
77 DateTime,
79}
80
81#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
83#[serde(rename_all = "camelCase")]
84pub struct StringSchema {
85 #[serde(rename = "type")]
86 pub type_: StringTypeConst,
87 #[serde(skip_serializing_if = "Option::is_none")]
88 pub title: Option<Cow<'static, str>>,
89 #[serde(skip_serializing_if = "Option::is_none")]
90 pub description: Option<Cow<'static, str>>,
91 #[serde(skip_serializing_if = "Option::is_none")]
92 pub min_length: Option<u32>,
93 #[serde(skip_serializing_if = "Option::is_none")]
94 pub max_length: Option<u32>,
95 #[serde(skip_serializing_if = "Option::is_none")]
96 pub format: Option<StringFormat>,
97}
98
99impl Default for StringSchema {
100 fn default() -> Self {
101 Self {
102 type_: StringTypeConst,
103 title: None,
104 description: None,
105 min_length: None,
106 max_length: None,
107 format: None,
108 }
109 }
110}
111
112impl StringSchema {
113 pub fn new() -> Self {
114 Self::default()
115 }
116
117 pub fn email() -> Self {
118 Self {
119 format: Some(StringFormat::Email),
120 ..Default::default()
121 }
122 }
123
124 pub fn uri() -> Self {
125 Self {
126 format: Some(StringFormat::Uri),
127 ..Default::default()
128 }
129 }
130
131 pub fn date() -> Self {
132 Self {
133 format: Some(StringFormat::Date),
134 ..Default::default()
135 }
136 }
137
138 pub fn date_time() -> Self {
139 Self {
140 format: Some(StringFormat::DateTime),
141 ..Default::default()
142 }
143 }
144
145 pub fn title(mut self, title: impl Into<Cow<'static, str>>) -> Self {
146 self.title = Some(title.into());
147 self
148 }
149
150 pub fn description(mut self, description: impl Into<Cow<'static, str>>) -> Self {
151 self.description = Some(description.into());
152 self
153 }
154
155 pub fn with_length(mut self, min: u32, max: u32) -> Result<Self, &'static str> {
156 if min > max {
157 return Err("min_length must be <= max_length");
158 }
159 self.min_length = Some(min);
160 self.max_length = Some(max);
161 Ok(self)
162 }
163
164 pub fn length(mut self, min: u32, max: u32) -> Self {
165 assert!(min <= max, "min_length must be <= max_length");
166 self.min_length = Some(min);
167 self.max_length = Some(max);
168 self
169 }
170
171 pub fn min_length(mut self, min: u32) -> Self {
172 self.min_length = Some(min);
173 self
174 }
175
176 pub fn max_length(mut self, max: u32) -> Self {
177 self.max_length = Some(max);
178 self
179 }
180
181 pub fn format(mut self, format: StringFormat) -> Self {
182 self.format = Some(format);
183 self
184 }
185}
186
187#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
196#[serde(rename_all = "camelCase")]
197pub struct NumberSchema {
198 #[serde(rename = "type")]
199 pub type_: NumberTypeConst,
200 #[serde(skip_serializing_if = "Option::is_none")]
201 pub title: Option<Cow<'static, str>>,
202 #[serde(skip_serializing_if = "Option::is_none")]
203 pub description: Option<Cow<'static, str>>,
204 #[serde(skip_serializing_if = "Option::is_none")]
205 pub minimum: Option<f64>,
206 #[serde(skip_serializing_if = "Option::is_none")]
207 pub maximum: Option<f64>,
208}
209
210impl Default for NumberSchema {
211 fn default() -> Self {
212 Self {
213 type_: NumberTypeConst,
214 title: None,
215 description: None,
216 minimum: None,
217 maximum: None,
218 }
219 }
220}
221
222impl NumberSchema {
223 pub fn new() -> Self {
224 Self::default()
225 }
226
227 pub fn with_range(mut self, min: f64, max: f64) -> Result<Self, &'static str> {
228 if min > max {
229 return Err("minimum must be <= maximum");
230 }
231 self.minimum = Some(min);
232 self.maximum = Some(max);
233 Ok(self)
234 }
235
236 pub fn range(mut self, min: f64, max: f64) -> Self {
237 assert!(min <= max, "minimum must be <= maximum");
238 self.minimum = Some(min);
239 self.maximum = Some(max);
240 self
241 }
242
243 pub fn minimum(mut self, min: f64) -> Self {
244 self.minimum = Some(min);
245 self
246 }
247
248 pub fn maximum(mut self, max: f64) -> Self {
249 self.maximum = Some(max);
250 self
251 }
252
253 pub fn title(mut self, title: impl Into<Cow<'static, str>>) -> Self {
254 self.title = Some(title.into());
255 self
256 }
257
258 pub fn description(mut self, description: impl Into<Cow<'static, str>>) -> Self {
259 self.description = Some(description.into());
260 self
261 }
262}
263
264#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
266#[serde(rename_all = "camelCase")]
267pub struct IntegerSchema {
268 #[serde(rename = "type")]
269 pub type_: IntegerTypeConst,
270 #[serde(skip_serializing_if = "Option::is_none")]
271 pub title: Option<Cow<'static, str>>,
272 #[serde(skip_serializing_if = "Option::is_none")]
273 pub description: Option<Cow<'static, str>>,
274 #[serde(skip_serializing_if = "Option::is_none")]
275 pub minimum: Option<i64>,
276 #[serde(skip_serializing_if = "Option::is_none")]
277 pub maximum: Option<i64>,
278}
279
280impl Default for IntegerSchema {
281 fn default() -> Self {
282 Self {
283 type_: IntegerTypeConst,
284 title: None,
285 description: None,
286 minimum: None,
287 maximum: None,
288 }
289 }
290}
291
292impl IntegerSchema {
293 pub fn new() -> Self {
294 Self::default()
295 }
296
297 pub fn with_range(mut self, min: i64, max: i64) -> Result<Self, &'static str> {
298 if min > max {
299 return Err("minimum must be <= maximum");
300 }
301 self.minimum = Some(min);
302 self.maximum = Some(max);
303 Ok(self)
304 }
305
306 pub fn range(mut self, min: i64, max: i64) -> Self {
307 assert!(min <= max, "minimum must be <= maximum");
308 self.minimum = Some(min);
309 self.maximum = Some(max);
310 self
311 }
312
313 pub fn minimum(mut self, min: i64) -> Self {
314 self.minimum = Some(min);
315 self
316 }
317
318 pub fn maximum(mut self, max: i64) -> Self {
319 self.maximum = Some(max);
320 self
321 }
322
323 pub fn title(mut self, title: impl Into<Cow<'static, str>>) -> Self {
324 self.title = Some(title.into());
325 self
326 }
327
328 pub fn description(mut self, description: impl Into<Cow<'static, str>>) -> Self {
329 self.description = Some(description.into());
330 self
331 }
332}
333
334#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
336#[serde(rename_all = "camelCase")]
337pub struct BooleanSchema {
338 #[serde(rename = "type")]
339 pub type_: BooleanTypeConst,
340 #[serde(skip_serializing_if = "Option::is_none")]
341 pub title: Option<Cow<'static, str>>,
342 #[serde(skip_serializing_if = "Option::is_none")]
343 pub description: Option<Cow<'static, str>>,
344 #[serde(skip_serializing_if = "Option::is_none")]
345 pub default: Option<bool>,
346}
347
348impl Default for BooleanSchema {
349 fn default() -> Self {
350 Self {
351 type_: BooleanTypeConst,
352 title: None,
353 description: None,
354 default: None,
355 }
356 }
357}
358
359impl BooleanSchema {
360 pub fn new() -> Self {
361 Self::default()
362 }
363
364 pub fn title(mut self, title: impl Into<Cow<'static, str>>) -> Self {
365 self.title = Some(title.into());
366 self
367 }
368
369 pub fn description(mut self, description: impl Into<Cow<'static, str>>) -> Self {
370 self.description = Some(description.into());
371 self
372 }
373
374 pub fn with_default(mut self, default: bool) -> Self {
375 self.default = Some(default);
376 self
377 }
378}
379
380#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
384pub struct EnumSchema; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
388#[serde(rename_all = "camelCase")]
389pub struct ElicitationSchema {
390 #[serde(rename = "type")]
391 pub type_: ObjectTypeConst,
392 #[serde(skip_serializing_if = "Option::is_none")]
393 pub title: Option<Cow<'static, str>>,
394 pub properties: BTreeMap<String, PrimitiveSchema>,
395 #[serde(skip_serializing_if = "Option::is_none")]
396 pub required: Option<Vec<String>>,
397 #[serde(skip_serializing_if = "Option::is_none")]
398 pub description: Option<Cow<'static, str>>,
399}
400
401impl ElicitationSchema {
402 pub fn new(properties: BTreeMap<String, PrimitiveSchema>) -> Self {
403 Self {
404 type_: ObjectTypeConst,
405 title: None,
406 properties,
407 required: None,
408 description: None,
409 }
410 }
411
412 pub fn builder() -> ElicitationSchemaBuilder {
413 ElicitationSchemaBuilder::new()
414 }
415}
416
417#[derive(Debug, Default)]
419pub struct ElicitationSchemaBuilder {
420 pub properties: BTreeMap<String, PrimitiveSchema>,
421 pub required: Vec<String>,
422 pub title: Option<Cow<'static, str>>,
423 pub description: Option<Cow<'static, str>>,
424}
425
426impl ElicitationSchemaBuilder {
427 pub fn new() -> Self {
428 Self::default()
429 }
430
431 pub fn property(mut self, name: impl Into<String>, schema: PrimitiveSchema) -> Self {
432 self.properties.insert(name.into(), schema);
433 self
434 }
435
436 pub fn required_property(mut self, name: impl Into<String>, schema: PrimitiveSchema) -> Self {
437 let name_str = name.into();
438 self.required.push(name_str.clone());
439 self.properties.insert(name_str, schema);
440 self
441 }
442
443 pub fn required_email(self, name: impl Into<String>) -> Self {
444 self.required_property(name, PrimitiveSchema::String(StringSchema::email()))
445 }
446
447 pub fn optional_bool(self, name: impl Into<String>, default: bool) -> Self {
448 self.property(
449 name,
450 PrimitiveSchema::Boolean(BooleanSchema::new().with_default(default)),
451 )
452 }
453
454 pub fn required_integer(self, name: impl Into<String>, min: i64, max: i64) -> Self {
455 self.required_property(
456 name,
457 PrimitiveSchema::Integer(IntegerSchema::new().range(min, max)),
458 )
459 }
460
461 pub fn description(mut self, description: impl Into<Cow<'static, str>>) -> Self {
462 self.description = Some(description.into());
463 self
464 }
465
466 pub fn build(self) -> Result<ElicitationSchema, &'static str> {
467 if !self.required.is_empty() {
469 for field_name in &self.required {
470 if !self.properties.contains_key(field_name) {
471 return Err("Required field does not exist in properties");
472 }
473 }
474 }
475
476 Ok(ElicitationSchema {
477 type_: ObjectTypeConst,
478 title: self.title,
479 properties: self.properties,
480 required: if self.required.is_empty() {
481 None
482 } else {
483 Some(self.required)
484 },
485 description: self.description,
486 })
487 }
488
489 pub fn build_unchecked(self) -> ElicitationSchema {
490 self.build().expect("Invalid elicitation schema")
491 }
492}